[
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Build GUI.for.SingBox\n\non:\n  push:\n    tags:\n      - \"v*\"\n\npermissions:\n  contents: write\n\njobs:\n  Build-Frontend:\n    runs-on: ubuntu-latest\n    if: github.repository == 'GUI-for-Cores/GUI.for.SingBox'\n    steps:\n      - uses: actions/checkout@v6\n      - uses: pnpm/action-setup@v4\n        with:\n          version: latest\n      - uses: actions/setup-node@v6\n        with:\n          node-version: \"latest\"\n          cache: \"pnpm\"\n          cache-dependency-path: frontend/pnpm-lock.yaml\n      - run: |\n          cd frontend\n          pnpm install --frozen-lockfile\n          pnpm build-only\n      - uses: actions/upload-artifact@v6\n        with:\n          name: frontend-dist\n          path: frontend/dist\n\n  Build-Windows:\n    needs: Build-Frontend\n    runs-on: windows-latest\n    env:\n      APP_NAME: GUI.for.SingBox\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          check-latest: true\n      - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest\n      - uses: actions/download-artifact@v8\n        with:\n          name: frontend-dist\n          path: frontend/dist\n\n      - name: Build & Pack Windows\n        shell: pwsh\n        run: |\n          function Build-And-Pack {\n            param([string]$arch)\n            $env:GOOS=\"windows\"\n            $env:GOARCH=$arch\n\n            Write-Host \"==> Building Windows $arch...\"\n            ~/go/bin/wails build -m -s -trimpath -skipbindings -devtools -tags webkit2_41 -o \"$env:APP_NAME.exe\"\n\n            cd build/bin\n            $zipName = \"$env:APP_NAME-windows-$arch.zip\"\n            Compress-Archive -Path \"$env:APP_NAME.exe\" -DestinationPath $zipName -Force\n            cd ../..\n          }\n\n          $arches = @(\"amd64\",\"arm64\",\"386\")\n          foreach ($arch in $arches) { Build-And-Pack $arch }\n\n      - uses: actions/upload-artifact@v6\n        with:\n          name: windows-builds\n          path: build/bin/*.zip\n\n  Build-macOS:\n    needs: Build-Frontend\n    runs-on: macos-latest\n    env:\n      APP_NAME: GUI.for.SingBox\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          check-latest: true\n      - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest\n      - uses: actions/download-artifact@v8\n        with:\n          name: frontend-dist\n          path: frontend/dist\n      - run: |\n          go mod vendor\n          sed -i \"\" \"s/\\[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular\\]/[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]/g\" vendor/github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin/AppDelegate.m\n\n      - name: Build & Pack macOS\n        run: |\n          build_and_pack() {\n            arch=$1\n            export GOOS=darwin GOARCH=$arch\n\n            echo \"==> Building macOS $arch...\"\n            ~/go/bin/wails build -m -s -trimpath -skipbindings -devtools -tags webkit2_41 -o $APP_NAME.exe\n\n            cd build/bin\n            zip -q -r $APP_NAME-darwin-$arch.zip $APP_NAME.app\n            cd ../..\n          }\n\n          for arch in amd64 arm64; do build_and_pack $arch; done\n\n      - uses: actions/upload-artifact@v6\n        with:\n          name: macos-builds\n          path: build/bin/*.zip\n\n  Build-Linux:\n    needs: Build-Frontend\n    runs-on: ubuntu-latest\n    env:\n      APP_NAME: GUI.for.SingBox\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          check-latest: true\n      - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest\n      - uses: actions/download-artifact@v8\n        with:\n          name: frontend-dist\n          path: frontend/dist\n      - run: |\n          sudo apt-get update\n          sudo apt-get install libgtk-3-dev libwebkit2gtk-4.1-dev\n\n      - name: Build & Pack Linux\n        run: |\n          build_and_pack() {\n            arch=$1\n            export GOOS=linux GOARCH=$arch\n\n            echo \"==> Building Linux $arch...\"\n            ~/go/bin/wails build -m -s -trimpath -skipbindings -devtools -tags webkit2_41 -o $APP_NAME.exe\n\n            cd build/bin\n            mv $APP_NAME.exe $APP_NAME\n            zip $APP_NAME-linux-$arch.zip $APP_NAME\n            cd ../..\n          }\n\n          build_and_pack amd64\n\n      - uses: actions/upload-artifact@v6\n        with:\n          name: linux-builds\n          path: build/bin/*.zip\n\n  Release:\n    needs: [Build-Windows, Build-macOS, Build-Linux]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/download-artifact@v8\n        with:\n          path: release-assets\n      - name: Create Release\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ github.ref_name }}\n          name: ${{ github.ref_name }}\n          files: release-assets/**/*.zip\n          draft: false\n          prerelease: ${{ contains(github.ref_name, 'dev') }}\n          body: |\n            Auto-generated release from GitHub Actions.\n"
  },
  {
    "path": ".github/workflows/rolling-release.yml",
    "content": "name: Rolling Release\n\non:\n  push:\n    branches: [main]\n    paths:\n      - \"frontend/**\"\n\n  workflow_dispatch:\n\njobs:\n  Build:\n    permissions: write-all\n    runs-on: ubuntu-latest\n    if: github.repository == 'GUI-for-Cores/GUI.for.SingBox'\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: latest\n      - name: Set up Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: \"latest\"\n          cache: \"pnpm\"\n          cache-dependency-path: frontend/pnpm-lock.yaml\n      - name: Install dependencies\n        run: cd frontend && pnpm install --frozen-lockfile\n      - name: Build Frontend\n        run: cd frontend && pnpm build-only\n      - name: Create a compressed file\n        run: |\n          git rev-parse --short HEAD | tr -d '\\n' > frontend/dist/version.txt\n          cd frontend\n          mv dist rolling-release\n          zip -r rolling-release.zip rolling-release\n      - name: Generate Changelog\n        run: |\n          set +e\n          LAST_COMMIT=$(curl -L https://github.com/GUI-for-Cores/GUI.for.SingBox/releases/download/rolling-release/version.txt)\n          echo -e \"## Change log\\n\\n> Update time: $(TZ='Asia/Shanghai' date \"+%Y-%m-%d %H:%M:%S\")\\n\" > changelog.md\n          git log $LAST_COMMIT..HEAD --pretty=format:\"* %s\" >> changelog.md\n          if [ $? -ne 0 ]; then\n            echo \"No changes found since last commit.\" >> changelog.md\n          fi\n          set -e\n      - name: Create Release and Upload Assets\n        uses: svenstaro/upload-release-action@v2\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          file: ./{frontend/{rolling-release.zip,rolling-release/version.txt},changelog.md}\n          file_glob: true\n          tag: rolling-release\n          release_name: rolling-release\n          overwrite: true\n          draft: false\n          prerelease: true\n          body: |\n            Rolling release built by GitHub Actions.\n            To use this version, please install the \"Rolling Release Assistant\" plugin and enable \"Enable Rolling Release\" within the app.\n"
  },
  {
    "path": ".gitignore",
    "content": "build/bin\nfrontend/dist\n\n.DS_Store"
  },
  {
    "path": "GUI.for.SingBox.code-workspace",
    "content": "{\n  \"folders\": [\n    {\n      \"path\": \".\"\n    },\n    {\n      \"path\": \"frontend\"\n    }\n  ],\n  \"settings\": {\n    \"oxc.enable\": true\n  }\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <img src=\"build/appicon.png\" alt=\"GUI.for.SingBox\" width=\"200\">\n  <h1>GUI.for.SingBox</h1>\n  <p>A GUI program developed by vue3 + wails.</p>\n</div>\n\n## Preview\n\nTake a look at the live version here: 👉 <a href=\"https://gui-for-cores.github.io/guide/gfs/\" target=\"_blank\">Live Demo</a>\n\n<div align=\"center\">\n  <img src=\"docs/imgs/light.png\">\n</div>\n\n## Document\n\n[Community](https://gui-for-cores.github.io/guide/gfs/community)\n\n## Build\n\n1、Build Environment\n\n- Node.js [link](https://nodejs.org/en)\n\n- pnpm ：`npm i -g pnpm`\n\n- Go [link](https://go.dev/)\n\n- Wails [link](https://wails.io/) ：`go install github.com/wailsapp/wails/v2/cmd/wails@latest`\n\n2、Pull and Build\n\n```bash\ngit clone https://github.com/GUI-for-Cores/GUI.for.SingBox.git\n\ncd GUI.for.SingBox/frontend\n\npnpm install --frozen-lockfile && pnpm build\n\ncd ..\n\nwails build\n```\n\n## Stargazers over time\n\n[![Stargazers over time](https://starchart.cc/GUI-for-Cores/GUI.for.SingBox.svg)](https://starchart.cc/GUI-for-Cores/GUI.for.SingBox)\n"
  },
  {
    "path": "bridge/bridge.go",
    "content": "package bridge\n\nimport (\n\t\"embed\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\tsysruntime \"runtime\"\n\n\t\"github.com/wailsapp/wails/v2/pkg/menu\"\n\t\"github.com/wailsapp/wails/v2/pkg/menu/keys\"\n\t\"github.com/wailsapp/wails/v2/pkg/options\"\n\t\"github.com/wailsapp/wails/v2/pkg/runtime\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nvar Config = &AppConfig{}\n\nvar Env = &EnvResult{\n\tIsStartup:    true,\n\tPreventExit:  true,\n\tFromTaskSch:  false,\n\tWebviewPath:  \"\",\n\tAppName:      \"\",\n\tAppVersion:   \"v1.21.0\",\n\tBasePath:     \"\",\n\tOS:           sysruntime.GOOS,\n\tARCH:         sysruntime.GOARCH,\n\tIsPrivileged: false,\n}\n\n// NewApp creates a new App application struct\nfunc NewApp() *App {\n\treturn &App{\n\t\tAppMenu: menu.NewMenu(),\n\t}\n}\n\nfunc CreateApp(fs embed.FS) *App {\n\texePath, err := os.Executable()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tEnv.BasePath = filepath.ToSlash(filepath.Dir(exePath))\n\tEnv.AppName = filepath.Base(exePath)\n\n\tif slices.Contains(os.Args, \"tasksch\") {\n\t\tEnv.FromTaskSch = true\n\t}\n\n\tif priv, err := IsPrivileged(); err == nil {\n\t\tEnv.IsPrivileged = priv\n\t}\n\n\tapp := NewApp()\n\n\tif Env.OS == \"darwin\" {\n\t\tcreateMacOSSymlink()\n\t\tcreateMacOSMenus(app)\n\t}\n\n\tif Env.OS == \"windows\" {\n\t\tprocessFixedWebView2Runtime()\n\t}\n\n\textractEmbeddedFiles(fs)\n\n\tloadConfig()\n\n\treturn app\n}\n\nfunc (a *App) IsStartup() bool {\n\tif Env.IsStartup {\n\t\tEnv.IsStartup = false\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (a *App) ExitApp() {\n\tlog.Printf(\"ExitApp\")\n\tEnv.PreventExit = false\n\truntime.Quit(a.Ctx)\n}\n\nfunc (a *App) RestartApp() FlagResult {\n\tlog.Printf(\"RestartApp\")\n\texePath := Env.BasePath + \"/\" + Env.AppName\n\n\tcmd := exec.Command(exePath)\n\tSetCmdWindowHidden(cmd)\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\ta.ExitApp()\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) GetEnv() EnvResult {\n\tlog.Printf(\"GetEnv\")\n\treturn EnvResult{\n\t\tAppName:      Env.AppName,\n\t\tAppVersion:   Env.AppVersion,\n\t\tBasePath:     Env.BasePath,\n\t\tOS:           Env.OS,\n\t\tARCH:         Env.ARCH,\n\t\tIsPrivileged: Env.IsPrivileged,\n\t}\n}\n\nfunc (a *App) GetInterfaces() FlagResult {\n\tlog.Printf(\"GetInterfaces\")\n\n\tinterfaces, err := net.Interfaces()\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tvar interfaceNames []string\n\n\tfor _, inter := range interfaces {\n\t\tinterfaceNames = append(interfaceNames, inter.Name)\n\t}\n\n\treturn FlagResult{true, strings.Join(interfaceNames, \"|\")}\n}\n\nfunc (a *App) ShowMainWindow() {\n\tlog.Printf(\"ShowMainWindow\")\n\truntime.WindowShow(a.Ctx)\n}\n\nfunc createMacOSSymlink() {\n\tuser, _ := user.Current()\n\tlinkPath := Env.BasePath + \"/data\"\n\tappPath := \"/Users/\" + user.Username + \"/Library/Application Support/\" + Env.AppName\n\tos.MkdirAll(appPath, os.ModePerm)\n\tos.Symlink(appPath, linkPath)\n}\n\nfunc createMacOSMenus(app *App) {\n\tappMenu := app.AppMenu.AddSubmenu(\"App\")\n\tappMenu.AddText(\"Show\", keys.CmdOrCtrl(\"s\"), func(_ *menu.CallbackData) {\n\t\truntime.WindowShow(app.Ctx)\n\t})\n\tappMenu.AddText(\"Hide\", keys.CmdOrCtrl(\"h\"), func(_ *menu.CallbackData) {\n\t\truntime.WindowHide(app.Ctx)\n\t})\n\tappMenu.AddSeparator()\n\tappMenu.AddText(\"Quit\", keys.CmdOrCtrl(\"q\"), func(_ *menu.CallbackData) {\n\t\truntime.EventsEmit(app.Ctx, \"onExitApp\")\n\t})\n\n\t// on macos platform, we should append EditMenu to enable Cmd+C,Cmd+V,Cmd+Z... shortcut\n\tapp.AppMenu.Append(menu.EditMenu())\n}\n\nfunc processFixedWebView2Runtime() {\n\twebviewDir := filepath.Join(Env.BasePath, \"data\", \"WebView2\")\n\n\terr := filepath.Walk(webviewDir, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif !info.IsDir() && strings.EqualFold(info.Name(), \"msedgewebview2.exe\") {\n\t\t\tEnv.WebviewPath = filepath.Dir(path)\n\t\t\tlog.Printf(\"WebView2 runtime already exists at: %s\", Env.WebviewPath)\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error during recursive search: %v\\n\", err)\n\t\treturn\n\t}\n\n\tif Env.WebviewPath != \"\" {\n\t\treturn\n\t}\n\n\tentries, err := os.ReadDir(webviewDir)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to read directory: %v\\n\", err)\n\t\treturn\n\t}\n\n\tvar cabFile string\n\tfor _, e := range entries {\n\t\tif !e.IsDir() &&\n\t\t\tstrings.HasSuffix(strings.ToLower(e.Name()), \".cab\") &&\n\t\t\tstrings.Contains(e.Name(), \"Microsoft.WebView2.FixedVersionRuntime\") {\n\t\t\tcabFile = filepath.Join(webviewDir, e.Name())\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif cabFile == \"\" {\n\t\tlog.Println(\"No WebView2 .cab file found. Skipping extraction.\")\n\t\treturn\n\t}\n\n\tlog.Printf(\"Found CAB file: %s\\n\", cabFile)\n\n\tcmd := exec.Command(\"expand.exe\", \"-F:*\", cabFile, webviewDir)\n\tSetCmdWindowHidden(cmd)\n\n\tlog.Println(\"Extracting WebView2 Runtime...\")\n\tif err := cmd.Run(); err != nil {\n\t\tlog.Printf(\"Extraction failed: %v\\n\", err)\n\t\treturn\n\t}\n\n\tlog.Printf(\"WebView2 Runtime extracted successfully into: %s\\n\", webviewDir)\n\tEnv.WebviewPath = strings.TrimSuffix(cabFile, \".cab\")\n}\n\nfunc extractEmbeddedFiles(fs embed.FS) {\n\ticonSrc := \"frontend/dist/icons\"\n\ticonDst := \"data/.cache/icons\"\n\timgSrc := \"frontend/dist/imgs\"\n\timgDst := \"data/.cache/imgs\"\n\n\tos.MkdirAll(GetPath(iconDst), os.ModePerm)\n\tos.MkdirAll(GetPath(imgDst), os.ModePerm)\n\n\textractFiles(fs, iconSrc, iconDst)\n\textractFiles(fs, imgSrc, imgDst)\n}\n\nfunc extractFiles(fs embed.FS, srcDir, dstDir string) {\n\tfiles, _ := fs.ReadDir(srcDir)\n\tfor _, file := range files {\n\t\tfileName := file.Name()\n\t\tdstPath := GetPath(dstDir + \"/\" + fileName)\n\t\tif _, err := os.Stat(dstPath); os.IsNotExist(err) {\n\t\t\tlog.Printf(\"InitResources [%s]: %s\", dstDir, fileName)\n\t\t\tdata, _ := fs.ReadFile(srcDir + \"/\" + fileName)\n\t\t\tif err := os.WriteFile(dstPath, data, os.ModePerm); err != nil {\n\t\t\t\tlog.Printf(\"Error writing file %s: %v\", dstPath, err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc loadConfig() {\n\tb, err := os.ReadFile(Env.BasePath + \"/data/user.yaml\")\n\tif err == nil {\n\t\tyaml.Unmarshal(b, &Config)\n\t}\n\n\tif Config.Width == 0 {\n\t\tConfig.Width = 800\n\t}\n\n\tif Config.Height == 0 {\n\t\tConfig.Height = 540\n\t}\n\n\tConfig.StartHidden = Env.FromTaskSch && Config.WindowStartState == int(options.Minimised)\n\n\tif !Env.FromTaskSch {\n\t\tConfig.WindowStartState = int(options.Normal)\n\t}\n}\n"
  },
  {
    "path": "bridge/exec.go",
    "content": "package bridge\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/shirou/gopsutil/v3/process\"\n\t\"github.com/wailsapp/wails/v2/pkg/runtime\"\n)\n\nfunc (a *App) Exec(path string, args []string, options ExecOptions) FlagResult {\n\tlog.Printf(\"Exec: %s %s %v\", path, args, options)\n\n\texePath := GetPath(path)\n\n\tif _, err := os.Stat(exePath); os.IsNotExist(err) {\n\t\texePath = path\n\t}\n\n\tcmd := exec.Command(exePath, args...)\n\tSetCmdWindowHidden(cmd)\n\n\tcmd.Dir = options.WorkingDirectory\n\tcmd.Env = os.Environ()\n\n\tfor key, value := range options.Env {\n\t\tcmd.Env = append(cmd.Env, key+\"=\"+value)\n\t}\n\n\tout, err := cmd.CombinedOutput()\n\n\tvar output string\n\tif options.Convert {\n\t\toutput = strings.TrimSpace(ConvertByte2String(out))\n\t} else {\n\t\toutput = strings.TrimSpace(string(out))\n\t}\n\n\tif err != nil {\n\t\tif output == \"\" {\n\t\t\toutput = err.Error()\n\t\t}\n\t\treturn FlagResult{false, output}\n\t}\n\n\treturn FlagResult{true, output}\n}\n\nfunc (a *App) ExecBackground(path string, args []string, outEvent string, endEvent string, options ExecOptions) FlagResult {\n\tlog.Printf(\"ExecBackground: %s %s %s %s %v\", path, args, outEvent, endEvent, options)\n\n\texePath := GetPath(path)\n\tpidPath := \"\"\n\n\tif _, err := os.Stat(exePath); os.IsNotExist(err) {\n\t\texePath = path\n\t}\n\n\tif options.PidFile != \"\" {\n\t\tpidPath = GetPath(options.PidFile)\n\t}\n\n\tcmd := exec.Command(exePath, args...)\n\tSetCmdWindowHidden(cmd)\n\n\tcmd.Dir = options.WorkingDirectory\n\tcmd.Env = os.Environ()\n\n\tfor key, value := range options.Env {\n\t\tcmd.Env = append(cmd.Env, key+\"=\"+value)\n\t}\n\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tcmd.Stderr = cmd.Stdout\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tpid := strconv.Itoa(cmd.Process.Pid)\n\n\tif pidPath != \"\" {\n\t\terr := os.WriteFile(pidPath, []byte(pid), os.ModePerm)\n\t\tif err != nil {\n\t\t\t_ = SendExitSignal(cmd.Process)\n\t\t\t_ = waitForProcessExitWithTimeout(cmd.Process, 10)\n\t\t\treturn FlagResult{false, err.Error()}\n\t\t}\n\t}\n\n\tif outEvent != \"\" {\n\t\tscanAndEmit := func(reader io.Reader) {\n\t\t\tscanner := bufio.NewScanner(reader)\n\t\t\tstopOutput := false\n\t\t\tfor scanner.Scan() {\n\t\t\t\tvar text string\n\t\t\t\tif options.Convert {\n\t\t\t\t\ttext = ConvertByte2String(scanner.Bytes())\n\t\t\t\t} else {\n\t\t\t\t\ttext = scanner.Text()\n\t\t\t\t}\n\n\t\t\t\tif !stopOutput {\n\t\t\t\t\truntime.EventsEmit(a.Ctx, outEvent, text)\n\n\t\t\t\t\tif options.StopOutputKeyword != \"\" && strings.Contains(text, options.StopOutputKeyword) {\n\t\t\t\t\t\tstopOutput = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tgo scanAndEmit(stdout)\n\t}\n\n\tif endEvent != \"\" {\n\t\tgo func() {\n\t\t\tcmd.Wait()\n\t\t\tif pidPath != \"\" {\n\t\t\t\t_ = os.Remove(pidPath)\n\t\t\t}\n\t\t\truntime.EventsEmit(a.Ctx, endEvent)\n\t\t}()\n\t}\n\n\treturn FlagResult{true, pid}\n}\n\nfunc (a *App) ProcessInfo(pid int32) FlagResult {\n\tlog.Printf(\"ProcessInfo: %d\", pid)\n\n\tproc, err := process.NewProcess(pid)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tname, err := proc.Name()\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, name}\n}\n\nfunc (a *App) ProcessMemory(pid int32) FlagResult {\n\tlog.Printf(\"ProcessMemory: %d\", pid)\n\n\tproc, err := process.NewProcess(pid)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tmemInfo, err := proc.MemoryInfo()\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, strconv.FormatUint(memInfo.RSS, 10)}\n}\n\nfunc (a *App) KillProcess(pid int, timeout int) FlagResult {\n\tlog.Printf(\"KillProcess: %d %d\", pid, timeout)\n\n\tprocess, err := os.FindProcess(pid)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tif err := SendExitSignal(process); err != nil {\n\t\tlog.Printf(\"SendExitSignal Err: %s\", err.Error())\n\t}\n\n\tif err := waitForProcessExitWithTimeout(process, timeout); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc waitForProcessExitWithTimeout(process *os.Process, timeoutSeconds int) error {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second)\n\tdefer cancel()\n\n\tinterval := 10 * time.Millisecond\n\tmaxInterval := 1000 * time.Millisecond\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tif killErr := process.Kill(); killErr != nil {\n\t\t\t\treturn fmt.Errorf(\"timed out after %d seconds waiting for process %d, and failed to kill it: %w\", timeoutSeconds, process.Pid, killErr)\n\t\t\t}\n\t\t\treturn nil\n\n\t\tdefault:\n\t\t\talive, err := IsProcessAlive(process)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to check status of process %d: %w\", process.Pid, err)\n\t\t\t}\n\t\t\tif !alive {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\ttime.Sleep(interval)\n\t\t\tinterval = min(time.Duration(interval*2), maxInterval)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "bridge/exec_others.go",
    "content": "//go:build !windows\n\npackage bridge\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"syscall\"\n)\n\nfunc SetCmdWindowHidden(cmd *exec.Cmd) {\n}\n\nfunc SendExitSignal(p *os.Process) error {\n\treturn p.Signal(syscall.SIGINT)\n}\n\nfunc IsProcessAlive(p *os.Process) (bool, error) {\n\terr := p.Signal(syscall.Signal(0))\n\tif err == nil {\n\t\treturn true, nil\n\t}\n\tif errors.Is(err, os.ErrProcessDone) {\n\t\treturn false, nil\n\t}\n\tif errno, ok := err.(syscall.Errno); ok {\n\t\tswitch errno {\n\t\tcase syscall.ESRCH:\n\t\t\treturn false, nil\n\t\tcase syscall.EPERM:\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, fmt.Errorf(\"failed to check process %d: %w\", p.Pid, err)\n}\n\nfunc IsPrivileged() (bool, error) {\n\treturn os.Geteuid() == 0, nil\n}\n"
  },
  {
    "path": "bridge/exec_windows.go",
    "content": "//go:build windows\n\npackage bridge\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"syscall\"\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\nconst ATTACH_PARENT_PROCESS uintptr = ^uintptr(0)\n\nvar (\n\tmodAdvapi32 = windows.NewLazySystemDLL(\"advapi32.dll\")\n\tmodKernel32 = windows.NewLazySystemDLL(\"kernel32.dll\")\n\n\tprocCheckTokenMembership     = modAdvapi32.NewProc(\"CheckTokenMembership\")\n\tprocFreeConsole              = modKernel32.NewProc(\"FreeConsole\")\n\tprocAttachConsole            = modKernel32.NewProc(\"AttachConsole\")\n\tprocSetConsoleCtrlHandler    = modKernel32.NewProc(\"SetConsoleCtrlHandler\")\n\tprocGenerateConsoleCtrlEvent = modKernel32.NewProc(\"GenerateConsoleCtrlEvent\")\n)\n\nfunc SetCmdWindowHidden(cmd *exec.Cmd) {\n\tcmd.SysProcAttr = &syscall.SysProcAttr{\n\t\tCreationFlags: windows.CREATE_UNICODE_ENVIRONMENT | windows.CREATE_NEW_PROCESS_GROUP,\n\t\tHideWindow:    true,\n\t}\n}\n\nfunc SendExitSignal(p *os.Process) error {\n\tif ret, _, err := procFreeConsole.Call(); ret == 0 && err != windows.ERROR_INVALID_HANDLE {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\tprocAttachConsole.Call(ATTACH_PARENT_PROCESS)\n\t}()\n\n\tif ret, _, err := procAttachConsole.Call(uintptr(p.Pid)); ret == 0 && err != windows.ERROR_ACCESS_DENIED {\n\t\treturn err\n\t}\n\n\tif ret, _, err := procSetConsoleCtrlHandler.Call(0, 1); ret == 0 {\n\t\treturn err\n\t}\n\n\tif ret, _, err := procGenerateConsoleCtrlEvent.Call(windows.CTRL_BREAK_EVENT, uintptr(p.Pid)); ret == 0 {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc IsProcessAlive(p *os.Process) (bool, error) {\n\th, err := windows.OpenProcess(windows.SYNCHRONIZE, false, uint32(p.Pid))\n\tif err != nil {\n\t\tif err == windows.ERROR_INVALID_PARAMETER {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\tdefer windows.CloseHandle(h)\n\n\ts, err := windows.WaitForSingleObject(h, 0)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tswitch s {\n\tcase windows.WAIT_OBJECT_0:\n\t\treturn false, nil\n\tcase uint32(windows.WAIT_TIMEOUT):\n\t\treturn true, nil\n\tdefault:\n\t\treturn false, fmt.Errorf(\"unexpected WaitForSingleObject status: %d\", s)\n\t}\n}\n\nfunc IsPrivileged() (bool, error) {\n\tvar sid *windows.SID\n\tsid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar isMember int32\n\n\tret, _, err := procCheckTokenMembership.Call(0, uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(&isMember)))\n\tif ret == 0 {\n\t\treturn false, err\n\t}\n\n\treturn isMember != 0, nil\n}\n"
  },
  {
    "path": "bridge/io.go",
    "content": "package bridge\n\nimport (\n\t\"archive/tar\"\n\t\"archive/zip\"\n\t\"compress/gzip\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/pkg/browser\"\n)\n\nconst (\n\tBinary = \"Binary\"\n\tText   = \"Text\"\n)\n\nfunc (a *App) WriteFile(path string, content string, options IOOptions) FlagResult {\n\tlog.Printf(\"WriteFile [%s %s]: %s\", options.Mode, options.Range, path)\n\n\tfullPath := GetPath(path)\n\n\tif err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tvar data []byte\n\tvar err error\n\n\tswitch options.Mode {\n\tcase Text:\n\t\tdata = []byte(content)\n\tcase Binary:\n\t\tdata, err = base64.StdEncoding.DecodeString(content)\n\t\tif err != nil {\n\t\t\treturn FlagResult{false, err.Error()}\n\t\t}\n\tdefault:\n\t\treturn FlagResult{false, \"Unsupported IO mode: \" + options.Mode}\n\t}\n\n\tfile, err := os.OpenFile(fullPath, os.O_RDWR|os.O_CREATE, 0644)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tdefer file.Close()\n\n\tstat, err := file.Stat()\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tfileSize := stat.Size()\n\n\tvar start, end int64\n\n\tif options.Range == \"\" {\n\t\tstart = 0\n\t\tend = int64(len(data)) - 1\n\n\t\tif err := file.Truncate(0); err != nil {\n\t\t\treturn FlagResult{false, err.Error()}\n\t\t}\n\t} else {\n\t\tstart, end, err = ParseRange(options.Range, fileSize)\n\t\tif err != nil {\n\t\t\treturn FlagResult{false, err.Error()}\n\t\t}\n\n\t\twriteLength := int64(len(data))\n\t\tif writeLength != end-start+1 {\n\t\t\treturn FlagResult{false, \"data length does not match range length\"}\n\t\t}\n\t}\n\n\t_, err = file.WriteAt(data, start)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) ReadFile(path string, options IOOptions) FlagResult {\n\tlog.Printf(\"ReadFile [%s %s]: %s\", options.Mode, options.Range, path)\n\n\tfullPath := GetPath(path)\n\n\tfile, err := os.Open(fullPath)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tdefer file.Close()\n\n\tstat, err := file.Stat()\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tfileSize := stat.Size()\n\n\tstart, end, err := ParseRange(options.Range, fileSize)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tlength := end - start + 1\n\tbuf := make([]byte, length)\n\n\tn, err := file.ReadAt(buf, start)\n\tif err != nil && err != io.EOF {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tbuf = buf[:n]\n\n\tswitch options.Mode {\n\tcase Text:\n\t\treturn FlagResult{true, string(buf)}\n\tcase Binary:\n\t\treturn FlagResult{true, base64.StdEncoding.EncodeToString(buf)}\n\tdefault:\n\t\treturn FlagResult{false, \"Unsupported IO mode: \" + options.Mode}\n\t}\n}\n\nfunc (a *App) MoveFile(source string, target string) FlagResult {\n\tlog.Printf(\"MoveFile: %s -> %s\", source, target)\n\n\tfullSource := GetPath(source)\n\tfullTarget := GetPath(target)\n\n\tif err := os.MkdirAll(filepath.Dir(fullTarget), os.ModePerm); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tif err := os.Rename(fullSource, fullTarget); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) RemoveFile(path string) FlagResult {\n\tlog.Printf(\"RemoveFile: %s\", path)\n\n\tfullPath := GetPath(path)\n\n\tif err := os.RemoveAll(fullPath); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) CopyFile(src string, dst string) FlagResult {\n\tlog.Printf(\"CopyFile: %s -> %s\", src, dst)\n\n\tsrcPath := GetPath(src)\n\tdstPath := GetPath(dst)\n\n\tsrcFile, err := os.Open(srcPath)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tdefer srcFile.Close()\n\n\tif err := os.MkdirAll(filepath.Dir(dstPath), os.ModePerm); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tdstFile, err := os.Create(dstPath)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tdefer dstFile.Close()\n\n\tif _, err := io.Copy(dstFile, srcFile); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) MakeDir(path string) FlagResult {\n\tlog.Printf(\"MakeDir: %s\", path)\n\n\tfullPath := GetPath(path)\n\n\tif err := os.MkdirAll(fullPath, os.ModePerm); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) ReadDir(path string) FlagResult {\n\tlog.Printf(\"ReadDir: %s\", path)\n\n\tfullPath := GetPath(path)\n\n\tfiles, err := os.ReadDir(fullPath)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tvar result []string\n\n\tfor _, file := range files {\n\t\tif info, err := file.Info(); err == nil {\n\t\t\tresult = append(result, fmt.Sprintf(\"%v,%v,%v\", info.Name(), info.Size(), info.IsDir()))\n\t\t}\n\t}\n\n\treturn FlagResult{true, strings.Join(result, \"|\")}\n}\n\nfunc (a *App) OpenDir(path string) FlagResult {\n\tlog.Printf(\"OpenDir: %s\", path)\n\n\tfullPath := GetPath(path)\n\n\terr := browser.OpenURL(fullPath)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) OpenURI(uri string) FlagResult {\n\tlog.Printf(\"OpenURI: %s\", uri)\n\n\terr := browser.OpenURL(uri)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) AbsolutePath(path string) FlagResult {\n\tlog.Printf(\"AbsolutePath: %s\", path)\n\n\tabsPath := GetPath(path)\n\n\treturn FlagResult{true, absPath}\n}\n\nfunc (a *App) UnzipZIPFile(path string, output string) FlagResult {\n\tlog.Printf(\"UnzipZIPFile: %s -> %s\", path, output)\n\n\tfullPath := GetPath(path)\n\toutputPath := GetPath(output)\n\n\tarchive, err := zip.OpenReader(fullPath)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tdefer archive.Close()\n\n\tcleanOutputPath := outputPath + \"/\"\n\n\tfor _, f := range archive.File {\n\t\tfilePath := filepath.ToSlash(filepath.Clean(filepath.Join(outputPath, f.Name)))\n\n\t\tif !strings.HasPrefix(filePath, cleanOutputPath) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif f.FileInfo().IsDir() {\n\t\t\tos.MkdirAll(filePath, os.ModePerm)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tfileInArchive, err := f.Open()\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tdstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())\n\t\tif err != nil {\n\t\t\tfileInArchive.Close()\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err := io.Copy(dstFile, fileInArchive); err != nil {\n\t\t\tfileInArchive.Close()\n\t\t\tdstFile.Close()\n\t\t\tcontinue\n\t\t}\n\n\t\tfileInArchive.Close()\n\t\tdstFile.Close()\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) UnzipTarGZFile(path string, output string) FlagResult {\n\tlog.Printf(\"UnzipTarGZFile: %s -> %s\", path, output)\n\n\tfullPath := GetPath(path)\n\toutputPath := GetPath(output)\n\n\tgzipFile, err := os.Open(fullPath)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tdefer gzipFile.Close()\n\n\tgzipReader, err := gzip.NewReader(gzipFile)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tdefer gzipReader.Close()\n\n\ttarReader := tar.NewReader(gzipReader)\n\n\tcleanOutputPath := outputPath + \"/\"\n\n\tfor {\n\t\theader, err := tarReader.Next()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn FlagResult{false, err.Error()}\n\t\t}\n\n\t\tfilePath := filepath.ToSlash(filepath.Clean(filepath.Join(outputPath, header.Name)))\n\n\t\tif !strings.HasPrefix(filePath, cleanOutputPath) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif header.Typeflag == tar.TypeDir {\n\t\t\tos.MkdirAll(filePath, os.ModePerm)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tdstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, header.FileInfo().Mode())\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err := io.Copy(dstFile, tarReader); err != nil {\n\t\t\tdstFile.Close()\n\t\t\tcontinue\n\t\t}\n\n\t\tdstFile.Close()\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) UnzipGZFile(path string, output string) FlagResult {\n\tlog.Printf(\"UnzipGZFile: %s -> %s\", path, output)\n\n\tfullPath := GetPath(path)\n\toutputPath := GetPath(output)\n\n\tgzipFile, err := os.Open(fullPath)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tdefer gzipFile.Close()\n\n\toutputFile, err := os.Create(outputPath)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tdefer outputFile.Close()\n\n\tgzipReader, err := gzip.NewReader(gzipFile)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\tdefer gzipReader.Close()\n\n\tif _, err := io.Copy(outputFile, gzipReader); err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) FileExists(path string) FlagResult {\n\tlog.Printf(\"FileExists: %s\", path)\n\n\tpath = GetPath(path)\n\n\t_, err := os.Stat(path)\n\tif err == nil {\n\t\treturn FlagResult{true, \"true\"}\n\t}\n\n\tif os.IsNotExist(err) {\n\t\treturn FlagResult{true, \"false\"}\n\t}\n\n\treturn FlagResult{false, err.Error()}\n}\n"
  },
  {
    "path": "bridge/mmdb.go",
    "content": "package bridge\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"log\"\n\t\"net\"\n\t\"sync\"\n\n\t\"github.com/oschwald/geoip2-golang\"\n)\n\ntype MMDBInstance = struct {\n\tRefs   map[string]bool\n\tReader *geoip2.Reader\n}\n\nvar (\n\tmu      sync.RWMutex\n\tmmdbMap = make(map[string]*MMDBInstance)\n)\n\nfunc (a *App) OpenMMDB(path string, id string) FlagResult {\n\tlog.Printf(\"OpenMMDB: %s -> %s\", id, path)\n\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\tif db, exists := mmdbMap[path]; exists {\n\t\tdb.Refs[id] = true\n\t\treturn FlagResult{true, \"Success\"}\n\t}\n\n\treader, err := geoip2.Open(GetPath(path))\n\tif err != nil {\n\t\treturn FlagResult{false, \"Failed to open mmdb: \" + err.Error()}\n\t}\n\n\tmmdbMap[path] = &MMDBInstance{\n\t\tRefs:   map[string]bool{id: true},\n\t\tReader: reader,\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) CloseMMDB(path string, id string) FlagResult {\n\tlog.Printf(\"CloseMMDB: %s -> %s\", id, path)\n\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\tdb, exists := mmdbMap[path]\n\n\tif !exists {\n\t\treturn FlagResult{false, \"Database not open: \" + path}\n\t}\n\n\tif !db.Refs[id] {\n\t\treturn FlagResult{false, \"Reference not found for: \" + id}\n\t}\n\n\tdelete(db.Refs, id)\n\n\tif len(db.Refs) == 0 {\n\t\tif err := db.Reader.Close(); err != nil {\n\t\t\treturn FlagResult{false, \"Failed to close reader: \" + err.Error()}\n\t\t}\n\t\tdelete(mmdbMap, path)\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) QueryMMDB(path string, ip string, dataType string) FlagResult {\n\tlog.Printf(\"QueryMMDB: %s -> %s\", path, ip)\n\n\tparsedIP := net.ParseIP(ip)\n\tif parsedIP == nil {\n\t\treturn FlagResult{false, \"Invalid IP address\"}\n\t}\n\n\tmu.RLock()\n\tdb, exists := mmdbMap[path]\n\tmu.RUnlock()\n\n\tif !exists {\n\t\treturn FlagResult{false, \"Database not open: \" + path}\n\t}\n\n\trecord, err := queryByType(db.Reader, parsedIP, dataType)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tbytes, err := json.Marshal(record)\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, string(bytes)}\n}\n\nfunc queryByType(reader *geoip2.Reader, ip net.IP, dataType string) (any, error) {\n\tswitch dataType {\n\tcase \"ASN\":\n\t\treturn reader.ASN(ip)\n\tcase \"AnonymousIP\":\n\t\treturn reader.AnonymousIP(ip)\n\tcase \"City\":\n\t\treturn reader.City(ip)\n\tcase \"ConnectionType\":\n\t\treturn reader.ConnectionType(ip)\n\tcase \"Country\":\n\t\treturn reader.Country(ip)\n\tcase \"Domain\":\n\t\treturn reader.Domain(ip)\n\tcase \"Enterprise\":\n\t\treturn reader.Enterprise(ip)\n\tdefault:\n\t\treturn nil, errors.New(\"Unsupported query type: \" + dataType)\n\t}\n}\n"
  },
  {
    "path": "bridge/net.go",
    "content": "package bridge\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"io\"\n\t\"log\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/wailsapp/wails/v2/pkg/runtime\"\n)\n\nfunc (a *App) Requests(method string, url string, headers map[string]string, body string, options RequestOptions) HTTPResult {\n\tlog.Printf(\"Requests: %v %v %v %v %v\", method, url, headers, body, options)\n\n\tclient, ctx, cancel := withRequestOptionsClient(options)\n\n\treq, err := http.NewRequestWithContext(ctx, method, url, strings.NewReader(body))\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\treq.Header = GetHeader(headers)\n\n\tif options.CancelId != \"\" {\n\t\truntime.EventsOn(a.Ctx, options.CancelId, func(data ...any) {\n\t\t\tlog.Printf(\"Requests Canceled: %v %v\", method, url)\n\t\t\tcancel()\n\t\t})\n\t\tdefer runtime.EventsOff(a.Ctx, options.CancelId)\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\tdefer resp.Body.Close()\n\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\treturn HTTPResult{true, resp.StatusCode, resp.Header, string(b)}\n}\n\nfunc (a *App) Download(method string, url string, path string, headers map[string]string, event string, options RequestOptions) HTTPResult {\n\tlog.Printf(\"Download: %s %s %s %v %s %v\", method, url, path, headers, event, options)\n\n\tclient, ctx, cancel := withRequestOptionsClient(options)\n\n\treq, err := http.NewRequestWithContext(ctx, method, url, nil)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\treq.Header = GetHeader(headers)\n\n\tif options.CancelId != \"\" {\n\t\truntime.EventsOn(a.Ctx, options.CancelId, func(data ...any) {\n\t\t\tlog.Printf(\"Download Canceled: %v %v\", url, path)\n\t\t\tcancel()\n\t\t})\n\t\tdefer runtime.EventsOff(a.Ctx, options.CancelId)\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\tdefer resp.Body.Close()\n\n\tpath = GetPath(path)\n\n\terr = os.MkdirAll(filepath.Dir(path), os.ModePerm)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\tfile, err := os.Create(path)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\tdefer file.Close()\n\n\treader := wrapWithProgress(resp.Body, resp.ContentLength, event, a)\n\n\t_, err = io.Copy(file, reader)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\treturn HTTPResult{true, resp.StatusCode, resp.Header, \"Success\"}\n}\n\nfunc (a *App) Upload(method string, url string, path string, headers map[string]string, event string, options RequestOptions) HTTPResult {\n\tlog.Printf(\"Upload: %s %s %s %v %s %v\", method, url, path, headers, event, options)\n\n\tpath = GetPath(path)\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\tdefer file.Close()\n\n\tfileStat, err := file.Stat()\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\n\tpart, err := writer.CreateFormFile(options.FileField, path)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\treader := wrapWithProgress(file, fileStat.Size(), event, a)\n\n\t_, err = io.Copy(part, reader)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\terr = writer.Close()\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\tclient, ctx, cancel := withRequestOptionsClient(options)\n\n\tif options.CancelId != \"\" {\n\t\truntime.EventsOn(a.Ctx, options.CancelId, func(data ...any) {\n\t\t\tlog.Printf(\"Upload Canceled: %v %v\", url, path)\n\t\t\tcancel()\n\t\t})\n\t\tdefer runtime.EventsOff(a.Ctx, options.CancelId)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, url, body)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\treq.Header = GetHeader(headers)\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\tdefer resp.Body.Close()\n\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn HTTPResult{false, 500, nil, err.Error()}\n\t}\n\n\treturn HTTPResult{true, resp.StatusCode, resp.Header, string(b)}\n}\n\nfunc (wt *WriteTracker) Write(p []byte) (n int, err error) {\n\tn = len(p)\n\twt.Progress += int64(n)\n\n\tshouldEmit := wt.Total <= 0 || wt.Progress-wt.LastEmitted >= wt.EmitThreshold || wt.Progress == wt.Total\n\tif shouldEmit {\n\t\truntime.EventsEmit(wt.App.Ctx, wt.ProgressChange, wt.Progress, wt.Total)\n\t\twt.LastEmitted = wt.Progress\n\t}\n\n\treturn n, nil\n}\n\nfunc wrapWithProgress(r io.Reader, size int64, event string, a *App) io.Reader {\n\tif event == \"\" {\n\t\treturn r\n\t}\n\treturn io.TeeReader(r, &WriteTracker{\n\t\tTotal:          size,\n\t\tEmitThreshold:  128 * 1024,\n\t\tProgressChange: event,\n\t\tApp:            a,\n\t})\n}\n\nfunc withRequestOptionsClient(options RequestOptions) (*http.Client, context.Context, context.CancelFunc) {\n\tclient := &http.Client{\n\t\tTimeout: GetTimeout(options.Timeout),\n\t\tTransport: &http.Transport{\n\t\t\tProxy: GetProxy(options.Proxy),\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tInsecureSkipVerify: options.Insecure,\n\t\t\t},\n\t\t},\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\tif !options.Redirect {\n\t\t\t\treturn http.ErrUseLastResponse\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\treturn client, ctx, cancel\n}\n"
  },
  {
    "path": "bridge/notification.go",
    "content": "package bridge\n\nimport (\n\t\"github.com/gen2brain/beeep\"\n)\n\nfunc (a *App) Notify(title string, message string, icon string, options NotifyOptions) FlagResult {\n\tfullPath := GetPath(icon)\n\n\tbeeep.AppName = options.AppName\n\n\tvar err error\n\tif options.Beep {\n\t\terr = beeep.Alert(title, message, fullPath)\n\t} else {\n\t\terr = beeep.Notify(title, message, fullPath)\n\t}\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\treturn FlagResult{true, \"Success\"}\n}\n"
  },
  {
    "path": "bridge/server.go",
    "content": "package bridge\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/wailsapp/wails/v2/pkg/runtime\"\n)\n\nvar requestCounter uint64\nvar serverMap sync.Map\n\ntype ResponseData struct {\n\tStatus  int\n\tHeaders map[string]string\n\tBody    string\n}\n\nfunc (a *App) StartServer(address string, serverID string, options ServerOptions) FlagResult {\n\tlog.Printf(\"StartServer: %s %s %v\", address, serverID, options)\n\n\tmux := http.NewServeMux()\n\n\tif options.StaticPath != \"\" && options.StaticRoute != \"\" {\n\t\tstatic := GetPath(options.StaticPath)\n\t\tfs := http.StripPrefix(options.StaticRoute, http.FileServer(http.Dir(static)))\n\n\t\tmux.HandleFunc(options.StaticRoute, func(w http.ResponseWriter, r *http.Request) {\n\t\t\thandleFileDownload(w, r, fs, options.StaticHeaders)\n\t\t})\n\t}\n\n\tif options.UploadPath != \"\" && options.UploadRoute != \"\" {\n\t\tuploadPath := GetPath(options.UploadPath)\n\t\tif err := os.MkdirAll(uploadPath, os.ModePerm); err != nil {\n\t\t\treturn FlagResult{false, \"Failed to create upload directory: \" + err.Error()}\n\t\t}\n\n\t\tmaxUploadSize := options.MaxUploadSize\n\t\tif maxUploadSize <= 0 {\n\t\t\tmaxUploadSize = 50 * 1024 * 1024 // 50MB\n\t\t}\n\n\t\tmux.HandleFunc(options.UploadRoute, func(w http.ResponseWriter, r *http.Request) {\n\t\t\thandleFileUpload(w, r, uploadPath, maxUploadSize, options.UploadHeaders)\n\t\t})\n\t}\n\n\tvar listener net.Listener\n\tif options.Cert != \"\" && options.Key != \"\" {\n\t\tcert, err := tls.LoadX509KeyPair(GetPath(options.Cert), GetPath(options.Key))\n\t\tif err != nil {\n\t\t\treturn FlagResult{false, \"Failed to load TLS cert: \" + err.Error()}\n\t\t}\n\t\ttlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}}\n\t\tln, err := net.Listen(\"tcp\", address)\n\t\tif err != nil {\n\t\t\treturn FlagResult{false, \"Failed to bind address: \" + err.Error()}\n\t\t}\n\t\tlistener = tls.NewListener(ln, tlsConfig)\n\t} else {\n\t\tln, err := net.Listen(\"tcp\", address)\n\t\tif err != nil {\n\t\t\treturn FlagResult{false, \"Failed to bind address: \" + err.Error()}\n\t\t}\n\t\tlistener = ln\n\t}\n\n\tmux.HandleFunc(\"/\", handleHttpRequest(a, serverID))\n\n\tserver := &http.Server{\n\t\tAddr:    address,\n\t\tHandler: mux,\n\t}\n\n\tgo func() {\n\t\tif err := server.Serve(listener); err != nil && err != http.ErrServerClosed {\n\t\t\tlog.Printf(\"Server error on %s: %v\", address, err)\n\t\t}\n\t}()\n\n\tserverMap.Store(serverID, server)\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) StopServer(id string) FlagResult {\n\tlog.Printf(\"StopServer: %s\", id)\n\n\tval, ok := serverMap.Load(id)\n\tif !ok {\n\t\treturn FlagResult{false, \"server not found\"}\n\t}\n\n\tserver, ok := val.(*http.Server)\n\tif !ok {\n\t\treturn FlagResult{false, \"invalid server type\"}\n\t}\n\n\terr := server.Close()\n\tif err != nil {\n\t\treturn FlagResult{false, err.Error()}\n\t}\n\n\tserverMap.Delete(id)\n\n\treturn FlagResult{true, \"Success\"}\n}\n\nfunc (a *App) ListServer() FlagResult {\n\tlog.Printf(\"ListServer\")\n\n\tvar servers []string\n\n\tserverMap.Range(func(key, value any) bool {\n\t\tserverID, ok := key.(string)\n\t\tif ok {\n\t\t\tservers = append(servers, serverID)\n\t\t}\n\t\treturn true\n\t})\n\n\treturn FlagResult{true, strings.Join(servers, \"|\")}\n}\n\nfunc handleHttpRequest(a *App, serverID string) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tr.Body = http.MaxBytesReader(w, r.Body, 20*1024*1024) // 20MB\n\t\tbody, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"Failed to read request body: \"+err.Error(), 500)\n\t\t\treturn\n\t\t}\n\n\t\tcount := atomic.AddUint64(&requestCounter, 1)\n\t\trequestID := serverID + strconv.FormatUint(count, 10)\n\t\trespChan := make(chan ResponseData, 1)\n\n\t\tctx, cancel := context.WithTimeout(a.Ctx, 60*time.Second) // 60s\n\t\tdefer cancel()\n\n\t\truntime.EventsOn(ctx, requestID, func(data ...any) {\n\t\t\tdefer runtime.EventsOff(ctx, requestID)\n\t\t\tresp := buildResponse(data)\n\t\t\trespChan <- resp\n\t\t})\n\n\t\truntime.EventsEmit(a.Ctx, serverID, requestID, r.Method, r.URL.RequestURI(), r.Header, body)\n\n\t\tselect {\n\t\tcase res := <-respChan:\n\t\t\tfor k, v := range res.Headers {\n\t\t\t\tw.Header().Set(k, v)\n\t\t\t}\n\t\t\tw.WriteHeader(res.Status)\n\t\t\tw.Write([]byte(res.Body))\n\t\tcase <-ctx.Done():\n\t\t\thttp.Error(w, \"Request timed out\", http.StatusGatewayTimeout)\n\t\t}\n\t}\n}\n\nfunc buildResponse(data []any) ResponseData {\n\tresp := ResponseData{Status: 200, Headers: make(map[string]string), Body: \"A sample http server\"}\n\tif len(data) >= 4 {\n\t\tif status, ok := data[0].(float64); ok {\n\t\t\tresp.Status = int(status)\n\t\t}\n\t\tif headers, ok := data[1].(string); ok {\n\t\t\tjson.Unmarshal([]byte(headers), &resp.Headers)\n\t\t}\n\t\tif body, ok := data[2].(string); ok {\n\t\t\tresp.Body = body\n\t\t}\n\t\tif optionsStr, ok := data[3].(string); ok {\n\t\t\tvar ioOptions IOOptions\n\t\t\tjson.Unmarshal([]byte(optionsStr), &ioOptions)\n\t\t\tif ioOptions.Mode == Binary {\n\t\t\t\tdecoded, err := base64.StdEncoding.DecodeString(resp.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tresp.Status = 500\n\t\t\t\t\tresp.Body = err.Error()\n\t\t\t\t} else {\n\t\t\t\t\tresp.Body = string(decoded)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn resp\n}\n\nfunc handleFileDownload(w http.ResponseWriter, r *http.Request, fs http.Handler, headers map[string]string) {\n\tfor key, value := range headers {\n\t\tw.Header().Set(key, value)\n\t}\n\tif r.Method == http.MethodOptions {\n\t\tw.WriteHeader(http.StatusNoContent)\n\t\treturn\n\t}\n\tfs.ServeHTTP(w, r)\n}\n\nfunc handleFileUpload(w http.ResponseWriter, r *http.Request, uploadPath string, maxUploadSize int64, headers map[string]string) {\n\tfor key, value := range headers {\n\t\tw.Header().Set(key, value)\n\t}\n\tif r.Method == http.MethodOptions {\n\t\tw.WriteHeader(http.StatusNoContent)\n\t\treturn\n\t}\n\tif r.Method != http.MethodPost && r.Method != http.MethodPut {\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t}\n\n\tr.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)\n\n\tcontentType := r.Header.Get(\"Content-Type\")\n\n\tif strings.HasPrefix(contentType, \"multipart/form-data\") {\n\t\thandleMultipartUpload(w, r, uploadPath)\n\t} else {\n\t\thandleRawUpload(w, r, uploadPath)\n\t}\n}\n\nfunc handleMultipartUpload(w http.ResponseWriter, r *http.Request, uploadPath string) {\n\treader, err := r.MultipartReader()\n\tif err != nil {\n\t\thttp.Error(w, \"Invalid multipart form: \"+err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tfor {\n\t\tpart, err := reader.NextPart()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"Error reading upload stream: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tif part.FileName() == \"\" {\n\t\t\tpart.Close()\n\t\t\tcontinue\n\t\t}\n\n\t\tdst, err := os.Create(filepath.Join(uploadPath, filepath.Base(part.FileName())))\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"Error creating file: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif _, err = io.Copy(dst, part); err != nil {\n\t\t\tdst.Close()\n\t\t\tpart.Close()\n\t\t\thttp.Error(w, \"Error saving file: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tdst.Close()\n\t\tpart.Close()\n\t}\n\n\tw.Write([]byte(\"File uploaded successfully\"))\n}\n\nfunc handleRawUpload(w http.ResponseWriter, r *http.Request, uploadPath string) {\n\tname := r.Header.Get(\"X-Filename\")\n\tif name == \"\" {\n\t\thttp.Error(w, \"Missing X-Filename\", 400)\n\t\treturn\n\t}\n\n\tdst, err := os.Create(filepath.Join(uploadPath, filepath.Base(name)))\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer dst.Close()\n\n\tif _, err := io.Copy(dst, r.Body); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write([]byte(\"File uploaded successfully\"))\n}\n"
  },
  {
    "path": "bridge/tray.go",
    "content": "package bridge\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/energye/systray\"\n\t\"github.com/wailsapp/wails/v2/pkg/runtime\"\n)\n\nfunc CreateTray(a *App, icon []byte) (trayStart, trayEnd func()) {\n\treturn systray.RunWithExternalLoop(func() {\n\t\tsystray.SetIcon(icon)\n\t\tsystray.SetTooltip(\"GUI.for.Cores\")\n\n\t\tsystray.SetOnRClick(func(menu systray.IMenu) { menu.ShowMenu() })\n\t\tsystray.SetOnClick(func(menu systray.IMenu) {\n\t\t\tif Env.OS == \"darwin\" {\n\t\t\t\tmenu.ShowMenu()\n\t\t\t} else {\n\t\t\t\ta.ShowMainWindow()\n\t\t\t}\n\t\t})\n\n\t\taddClickMenuItem := func(title, tooltip string, action func()) {\n\t\t\tm := systray.AddMenuItem(title, tooltip)\n\t\t\tm.Click(action)\n\t\t}\n\n\t\t// Ensure the tray is still available if rolling-release fails\n\t\taddClickMenuItem(\"Show\", \"Show\", func() { a.ShowMainWindow() })\n\t\taddClickMenuItem(\"Restart\", \"Restart\", func() { a.RestartApp() })\n\t\taddClickMenuItem(\"Exit\", \"Exit\", func() { a.ExitApp() })\n\t}, nil)\n}\n\nfunc (a *App) UpdateTray(tray TrayContent) {\n\tlog.Printf(\"UpdateTray\")\n\tupdateTray(a, tray)\n}\n\nfunc (a *App) UpdateTrayMenus(menus []MenuItem) {\n\tlog.Printf(\"UpdateTrayMenus\")\n\tupdateTrayMenus(a, menus)\n}\n\nfunc (a *App) UpdateTrayAndMenus(tray TrayContent, menus []MenuItem) {\n\tlog.Printf(\"UpdateTrayAndMenus\")\n\tupdateTray(a, tray)\n\tupdateTrayMenus(a, menus)\n}\n\nfunc createMenuItem(menu MenuItem, a *App, parent *systray.MenuItem) {\n\tif menu.Hidden {\n\t\treturn\n\t}\n\tswitch menu.Type {\n\tcase \"item\":\n\t\tvar m *systray.MenuItem\n\t\tif parent == nil {\n\t\t\tm = systray.AddMenuItem(menu.Text, menu.Tooltip)\n\t\t} else {\n\t\t\tm = parent.AddSubMenuItem(menu.Text, menu.Tooltip)\n\t\t}\n\n\t\tm.Click(func() { go runtime.EventsEmit(a.Ctx, \"onMenuItemClick\", menu.Event) })\n\n\t\tif menu.Checked {\n\t\t\tm.Check()\n\t\t}\n\n\t\tfor _, child := range menu.Children {\n\t\t\tcreateMenuItem(child, a, m)\n\t\t}\n\tcase \"separator\":\n\t\tsystray.AddSeparator()\n\t}\n}\n\nfunc updateTray(a *App, tray TrayContent) {\n\tif tray.Icon != \"\" {\n\t\tico, err := os.ReadFile(GetPath(tray.Icon))\n\t\tif err == nil {\n\t\t\tsystray.SetIcon(ico)\n\t\t}\n\t}\n\tif tray.Title != \"\" {\n\t\tsystray.SetTitle(tray.Title)\n\t\truntime.WindowSetTitle(a.Ctx, tray.Title)\n\t}\n\tif tray.Tooltip != \"\" {\n\t\tsystray.SetTooltip(tray.Tooltip)\n\t}\n}\n\nfunc updateTrayMenus(a *App, menus []MenuItem) {\n\tsystray.ResetMenu()\n\n\tfor _, menu := range menus {\n\t\tcreateMenuItem(menu, a, nil)\n\t}\n}\n"
  },
  {
    "path": "bridge/types.go",
    "content": "package bridge\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/wailsapp/wails/v2/pkg/menu\"\n)\n\n// App struct\ntype App struct {\n\tCtx     context.Context\n\tAppMenu *menu.Menu\n}\n\ntype EnvResult struct {\n\tIsStartup    bool   `json:\"-\"`\n\tPreventExit  bool   `json:\"-\"`\n\tFromTaskSch  bool   `json:\"-\"`\n\tWebviewPath  string `json:\"-\"`\n\tAppName      string `json:\"appName\"`\n\tAppVersion   string `json:\"appVersion\"`\n\tBasePath     string `json:\"basePath\"`\n\tOS           string `json:\"os\"`\n\tARCH         string `json:\"arch\"`\n\tIsPrivileged bool   `json:\"isPrivileged\"`\n}\n\ntype RequestOptions struct {\n\tProxy     string\n\tInsecure  bool\n\tRedirect  bool\n\tTimeout   int\n\tCancelId  string\n\tFileField string\n}\n\ntype ExecOptions struct {\n\tPidFile           string\n\tStopOutputKeyword string\n\tWorkingDirectory  string\n\tConvert           bool\n\tEnv               map[string]string\n}\n\ntype Range struct {\n\tStart *int64\n\tEnd   *int64\n}\n\ntype IOOptions struct {\n\tMode  string // Binary / Text\n\tRange string // \"start-end\" / \"start-\" / \"-end\"\n}\n\ntype FlagResult struct {\n\tFlag bool   `json:\"flag\"`\n\tData string `json:\"data\"`\n}\n\ntype ServerOptions struct {\n\tCert          string\n\tKey           string\n\tStaticPath    string\n\tStaticRoute   string\n\tStaticHeaders map[string]string\n\tUploadPath    string\n\tUploadRoute   string\n\tUploadHeaders map[string]string\n\tMaxUploadSize int64\n}\n\ntype NotifyOptions struct {\n\tAppName string\n\tBeep    bool\n}\n\ntype HTTPResult struct {\n\tFlag    bool        `json:\"flag\"`\n\tStatus  int         `json:\"status\"`\n\tHeaders http.Header `json:\"headers\"`\n\tBody    string      `json:\"body\"`\n}\n\ntype AppConfig struct {\n\tWindowStartState int  `yaml:\"windowStartState\"`\n\tWebviewGpuPolicy int  `yaml:\"webviewGpuPolicy\"`\n\tWidth            int  `yaml:\"width\"`\n\tHeight           int  `yaml:\"height\"`\n\tMultipleInstance bool `yaml:\"multipleInstance\"`\n\tRollingRelease   bool `yaml:\"rollingRelease\" default:\"true\"`\n\tStartHidden      bool\n}\n\ntype TrayContent struct {\n\tIcon    string `json:\"icon,omitempty\"`\n\tTitle   string `json:\"title,omitempty\"`\n\tTooltip string `json:\"tooltip,omitempty\"`\n}\n\ntype WriteTracker struct {\n\tTotal          int64\n\tProgress       int64\n\tLastEmitted    int64\n\tEmitThreshold  int64\n\tProgressChange string\n\tApp            *App\n}\n\ntype MenuItem struct {\n\tType     string     `json:\"type\"` // Menu Type: item / separator\n\tText     string     `json:\"text\"`\n\tTooltip  string     `json:\"tooltip\"`\n\tEvent    string     `json:\"event\"`\n\tChildren []MenuItem `json:\"children\"`\n\tHidden   bool       `json:\"hidden\"`\n\tChecked  bool       `json:\"checked\"`\n}\n"
  },
  {
    "path": "bridge/utils.go",
    "content": "package bridge\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/text/encoding/simplifiedchinese\"\n)\n\nfunc GetPath(path string) string {\n\tif !filepath.IsAbs(path) {\n\t\tpath = filepath.Join(Env.BasePath, path)\n\t}\n\treturn filepath.ToSlash(filepath.Clean(path))\n}\n\nfunc GetProxy(_proxy string) func(*http.Request) (*url.URL, error) {\n\tproxy := http.ProxyFromEnvironment\n\n\tif _proxy != \"\" {\n\t\tproxyUrl, err := url.Parse(_proxy)\n\t\tif err == nil {\n\t\t\tproxy = http.ProxyURL(proxyUrl)\n\t\t}\n\t}\n\n\treturn proxy\n}\n\nfunc GetTimeout(timeout int) time.Duration {\n\tif timeout <= 0 {\n\t\treturn 15 * time.Second\n\t}\n\treturn time.Duration(timeout) * time.Second\n}\n\nfunc GetHeader(headers map[string]string) http.Header {\n\theader := make(http.Header, len(headers))\n\tfor key, value := range headers {\n\t\theader.Set(key, value)\n\t}\n\treturn header\n}\n\nfunc ConvertByte2String(byte []byte) string {\n\tdecodeBytes, _ := simplifiedchinese.GB18030.NewDecoder().Bytes(byte)\n\treturn string(decodeBytes)\n}\n\nfunc ParseRange(s string, size int64) (start int64, end int64, err error) {\n\tif s == \"\" {\n\t\treturn 0, size - 1, nil\n\t}\n\n\ts = strings.TrimSpace(s)\n\n\t// \"bytes=100-200\"\n\ts = strings.TrimPrefix(s, \"bytes=\")\n\n\tparts := strings.SplitN(s, \"-\", 2)\n\tif len(parts) != 2 {\n\t\treturn 0, 0, errors.New(\"invalid range format\")\n\t}\n\n\tstartStr := strings.TrimSpace(parts[0])\n\tendStr := strings.TrimSpace(parts[1])\n\n\t// \"-200\" last 200 bytes\n\tif startStr == \"\" && endStr != \"\" {\n\t\te, err2 := strconv.ParseInt(endStr, 10, 64)\n\t\tif err2 != nil || e < 0 {\n\t\t\treturn 0, 0, errors.New(\"invalid range value\")\n\t\t}\n\t\tif e > size {\n\t\t\tstart = 0\n\t\t} else {\n\t\t\tstart = size - e\n\t\t}\n\t\tend = size - 1\n\t\treturn start, end, nil\n\t}\n\n\t// \"100-\" from start to EOF\n\tif startStr != \"\" && endStr == \"\" {\n\t\tstart, err = strconv.ParseInt(startStr, 10, 64)\n\t\tif err != nil || start < 0 {\n\t\t\treturn 0, 0, errors.New(\"invalid range value\")\n\t\t}\n\t\tend = size - 1\n\t\treturn start, end, nil\n\t}\n\n\t// \"100-200\"\n\tif startStr != \"\" && endStr != \"\" {\n\t\tstart, err = strconv.ParseInt(startStr, 10, 64)\n\t\tif err != nil || start < 0 {\n\t\t\treturn 0, 0, errors.New(\"invalid range value\")\n\t\t}\n\t\tend, err = strconv.ParseInt(endStr, 10, 64)\n\t\tif err != nil || end < 0 {\n\t\t\treturn 0, 0, errors.New(\"invalid range value\")\n\t\t}\n\t\tif start > end {\n\t\t\treturn 0, 0, errors.New(\"invalid range: start > end\")\n\t\t}\n\t\tif end >= size {\n\t\t\tend = size - 1\n\t\t}\n\t\treturn start, end, nil\n\t}\n\n\treturn 0, 0, errors.New(\"invalid range format\")\n}\n\nfunc RollingRelease(next http.Handler) http.Handler {\n\tisDevVersion := strings.Contains(Env.AppVersion, \"dev\")\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\turl := r.URL.Path\n\t\tisIndex := url == \"/\"\n\n\t\tif isIndex {\n\t\t\tw.Header().Set(\"Cache-Control\", \"no-cache\")\n\t\t} else {\n\t\t\tw.Header().Set(\"Cache-Control\", \"max-age=31536000, immutable\")\n\t\t}\n\n\t\tif isDevVersion || !Config.RollingRelease {\n\t\t\tnext.ServeHTTP(w, r)\n\t\t\treturn\n\t\t}\n\n\t\tif isIndex {\n\t\t\turl = \"/index.html\"\n\t\t}\n\n\t\tfilePath := GetPath(\"data/rolling-release\" + url)\n\t\tif _, err := os.Stat(filePath); err != nil {\n\t\t\tnext.ServeHTTP(w, r)\n\t\t\treturn\n\t\t}\n\n\t\thttp.ServeFile(w, r, filePath)\n\t})\n}\n"
  },
  {
    "path": "build/README.md",
    "content": "# Build Directory\n\nThe build directory is used to house all the build files and assets for your application. \n\nThe structure is:\n\n* bin - Output directory\n* darwin - macOS specific files\n* windows - Windows specific files\n\n## Mac\n\nThe `darwin` directory holds files specific to Mac builds.\nThese may be customised and used as part of the build. To return these files to the default state, simply delete them\nand\nbuild with `wails build`.\n\nThe directory contains the following files:\n\n- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.\n- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.\n\n## Windows\n\nThe `windows` directory contains the manifest and rc files used when building with `wails build`.\nThese may be customised for your application. To return these files to the default state, simply delete them and\nbuild with `wails build`.\n\n- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to\n  use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file\n  will be created using the `appicon.png` file in the build directory.\n- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.\n- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,\n  as well as the application itself (right click the exe -> properties -> details)\n- `wails.exe.manifest` - The main application manifest file."
  },
  {
    "path": "build/darwin/Info.dev.plist",
    "content": "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>CFBundlePackageType</key>\n        <string>APPL</string>\n        <key>CFBundleName</key>\n        <string>{{.Info.ProductName}}</string>\n        <key>CFBundleExecutable</key>\n        <string>{{.Name}}</string>\n        <key>CFBundleIdentifier</key>\n        <string>com.wails.{{.Name}}</string>\n        <key>CFBundleVersion</key>\n        <string>{{.Info.ProductVersion}}</string>\n        <key>CFBundleGetInfoString</key>\n        <string>{{.Info.Comments}}</string>\n        <key>CFBundleShortVersionString</key>\n        <string>{{.Info.ProductVersion}}</string>\n        <key>CFBundleIconFile</key>\n        <string>iconfile</string>\n        <key>LSMinimumSystemVersion</key>\n        <string>10.13.0</string>\n        <key>NSHighResolutionCapable</key>\n        <string>true</string>\n        <key>NSHumanReadableCopyright</key>\n        <string>{{.Info.Copyright}}</string>\n        {{if .Info.FileAssociations}}\n        <key>CFBundleDocumentTypes</key>\n        <array>\n          {{range .Info.FileAssociations}}\n          <dict>\n            <key>CFBundleTypeExtensions</key>\n            <array>\n              <string>{{.Ext}}</string>\n            </array>\n            <key>CFBundleTypeName</key>\n            <string>{{.Name}}</string>\n            <key>CFBundleTypeRole</key>\n            <string>{{.Role}}</string>\n            <key>CFBundleTypeIconFile</key>\n            <string>{{.IconName}}</string>\n          </dict>\n          {{end}}\n        </array>\n        {{end}}\n        {{if .Info.Protocols}}\n        <key>CFBundleURLTypes</key>\n        <array>\n          {{range .Info.Protocols}}\n            <dict>\n                <key>CFBundleURLName</key>\n                <string>com.wails.{{.Scheme}}</string>\n                <key>CFBundleURLSchemes</key>\n                <array>\n                    <string>{{.Scheme}}</string>\n                </array>\n                <key>CFBundleTypeRole</key>\n                <string>{{.Role}}</string>\n            </dict>\n          {{end}}\n        </array>\n        {{end}}\n        <key>NSAppTransportSecurity</key>\n        <dict>\n            <key>NSAllowsLocalNetworking</key>\n            <true/>\n        </dict>\n    </dict>\n</plist>\n"
  },
  {
    "path": "build/darwin/Info.plist",
    "content": "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>CFBundlePackageType</key>\n        <string>APPL</string>\n        <key>CFBundleName</key>\n        <string>{{.Info.ProductName}}</string>\n        <key>CFBundleExecutable</key>\n        <string>{{.Name}}</string>\n        <key>CFBundleIdentifier</key>\n        <string>com.wails.{{.Name}}</string>\n        <key>CFBundleVersion</key>\n        <string>{{.Info.ProductVersion}}</string>\n        <key>CFBundleGetInfoString</key>\n        <string>{{.Info.Comments}}</string>\n        <key>CFBundleShortVersionString</key>\n        <string>{{.Info.ProductVersion}}</string>\n        <key>CFBundleIconFile</key>\n        <string>iconfile</string>\n        <key>LSMinimumSystemVersion</key>\n        <string>10.13.0</string>\n        <key>NSHighResolutionCapable</key>\n        <string>true</string>\n        <key>NSHumanReadableCopyright</key>\n        <string>{{.Info.Copyright}}</string>\n        <key>LSUIElement</key>\n        <string>true</string>\n        {{if .Info.FileAssociations}}\n        <key>CFBundleDocumentTypes</key>\n        <array>\n          {{range .Info.FileAssociations}}\n          <dict>\n            <key>CFBundleTypeExtensions</key>\n            <array>\n              <string>{{.Ext}}</string>\n            </array>\n            <key>CFBundleTypeName</key>\n            <string>{{.Name}}</string>\n            <key>CFBundleTypeRole</key>\n            <string>{{.Role}}</string>\n            <key>CFBundleTypeIconFile</key>\n            <string>{{.IconName}}</string>\n          </dict>\n          {{end}}\n        </array>\n        {{end}}\n        {{if .Info.Protocols}}\n        <key>CFBundleURLTypes</key>\n        <array>\n          {{range .Info.Protocols}}\n            <dict>\n                <key>CFBundleURLName</key>\n                <string>com.wails.{{.Scheme}}</string>\n                <key>CFBundleURLSchemes</key>\n                <array>\n                    <string>{{.Scheme}}</string>\n                </array>\n                <key>CFBundleTypeRole</key>\n                <string>{{.Role}}</string>\n            </dict>\n          {{end}}\n        </array>\n        {{end}}\n    </dict>\n</plist>\n"
  },
  {
    "path": "build/windows/info.json",
    "content": "{\n\t\"fixed\": {\n\t\t\"file_version\": \"{{.Info.ProductVersion}}\"\n\t},\n\t\"info\": {\n\t\t\"0000\": {\n\t\t\t\"ProductVersion\": \"{{.Info.ProductVersion}}\",\n\t\t\t\"CompanyName\": \"{{.Info.CompanyName}}\",\n\t\t\t\"FileDescription\": \"{{.Info.ProductName}}\",\n\t\t\t\"LegalCopyright\": \"{{.Info.Copyright}}\",\n\t\t\t\"ProductName\": \"{{.Info.ProductName}}\",\n\t\t\t\"Comments\": \"{{.Info.Comments}}\"\n\t\t}\n\t}\n}"
  },
  {
    "path": "build/windows/wails.exe.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com:asm.v1\" xmlns:asmv3=\"urn:schemas-microsoft-com:asm.v3\">\n    <assemblyIdentity type=\"win32\" name=\"com.wails.{{.Name}}\" version=\"{{.Info.ProductVersion}}.0\" processorArchitecture=\"*\"/>\n    <dependency>\n        <dependentAssembly>\n            <assemblyIdentity type=\"win32\" name=\"Microsoft.Windows.Common-Controls\" version=\"6.0.0.0\" processorArchitecture=\"*\" publicKeyToken=\"6595b64144ccf1df\" language=\"*\"/>\n        </dependentAssembly>\n    </dependency>\n    <asmv3:application>\n        <asmv3:windowsSettings>\n            <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->\n            <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->\n        </asmv3:windowsSettings>\n    </asmv3:application>\n</assembly>"
  },
  {
    "path": "frontend/.editorconfig",
    "content": "[*.{ts,vue,less}]\ncharset = utf-8\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nend_of_line = lf\nmax_line_length = 100\n"
  },
  {
    "path": "frontend/.gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": "frontend/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Store\ndist\ndist-ssr\ncoverage\n*.local\n\n/cypress/videos/\n/cypress/screenshots/\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n!.vscode/settings.json\n.idea\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n*.tsbuildinfo\n\npackage.json.md5\n.eslintcache"
  },
  {
    "path": "frontend/.oxfmtrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxfmt/configuration_schema.json\",\n  \"semi\": false,\n  \"tabWidth\": 2,\n  \"singleQuote\": true,\n  \"printWidth\": 100,\n  \"trailingComma\": \"all\",\n  \"ignorePatterns\": [\"src/bridge/wailsjs/**\"]\n}\n"
  },
  {
    "path": "frontend/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n  \"plugins\": [\"eslint\", \"typescript\", \"unicorn\", \"oxc\", \"vue\"],\n  \"env\": {\n    \"browser\": true\n  },\n  \"categories\": {\n    \"correctness\": \"error\"\n  },\n  \"rules\": {\n    \"no-unused-expressions\": [\n      \"error\",\n      {\n        \"allowShortCircuit\": true,\n        \"allowTernary\": true\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "frontend/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"Vue.volar\",\n    \"dbaeumer.vscode-eslint\",\n    \"EditorConfig.EditorConfig\",\n    \"oxc.oxc-vscode\"\n  ]\n}\n"
  },
  {
    "path": "frontend/.vscode/settings.json",
    "content": "{\n  \"explorer.fileNesting.enabled\": true,\n  \"explorer.fileNesting.patterns\": {\n    \"tsconfig.json\": \"tsconfig.*.json, env.d.ts\",\n    \"package.json\": \"package-lock.json, pnpm*, .oxlint*, eslint*, .oxfmtrc*, .editorconfig\"\n  },\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll\": \"explicit\"\n  },\n  \"editor.formatOnSave\": true,\n  \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n}\n"
  },
  {
    "path": "frontend/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n  readonly VITE_APP_TITLE: string\n  readonly VITE_APP_VERSION: string\n  readonly VITE_APP_PROJECT_URL: string\n  readonly VITE_APP_LOCALES_URL: string\n  readonly VITE_APP_TG_GROUP: string\n  readonly VITE_APP_TG_CHANNEL: string\n  readonly VITE_APP_VERSION_API: string\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv\n}\n"
  },
  {
    "path": "frontend/eslint.config.js",
    "content": "import skipFormatting from 'eslint-config-prettier/flat'\nimport { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'\nimport { globalIgnores } from 'eslint/config'\nimport pluginOxlint from 'eslint-plugin-oxlint'\nimport pluginVue from 'eslint-plugin-vue'\n\nexport default defineConfigWithVueTs(\n  {\n    name: 'app/files-to-lint',\n    files: ['**/*.{ts,vue}'],\n  },\n\n  globalIgnores(['**/dist/**', '**/wailsjs/**']),\n\n  ...pluginVue.configs['flat/recommended'],\n  vueTsConfigs.recommended,\n\n  skipFormatting,\n\n  ...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),\n\n  {\n    rules: {\n      '@typescript-eslint/no-explicit-any': ['off'],\n      'vue/no-v-html': ['off'],\n      'vue/multi-word-component-names': [\n        'error',\n        {\n          ignores: ['index'],\n        },\n      ],\n    },\n  },\n)\n"
  },
  {
    "path": "frontend/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>%VITE_APP_TITLE%</title>\n  </head>\n  <body>\n    <div id=\"ipc-error\" style=\"display: none\" class=\"px-32 leading-relaxed\">\n      <h1>WebView2 Runtime Error</h1>\n      <p>\n        Your system's WebView2 Runtime version is not compatible with this software.<br />\n        Please follow the steps below to fix the issue:\n      </p>\n      <ol>\n        <li>\n          Go to\n          <a href=\"https://developer.microsoft.com/en-us/Microsoft-edge/webview2\" target=\"_blank\">\n            <b> https://developer.microsoft.com/en-us/Microsoft-edge/webview2 </b>\n          </a>\n          and download the fixed version.\n        </li>\n        <li>Create a <b>data/WebView2</b> folder in the same directory as the software.</li>\n        <li>Place the downloaded <code>.cab</code> file into this folder.</li>\n        <li>Restart the application.</li>\n      </ol>\n    </div>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n    <script>\n      if (!window.WailsInvoke) {\n        document.getElementById('ipc-error').style.display = 'block'\n        document.getElementById('app').style.display = 'none'\n      }\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n  \"name\": \"frontend\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite --host\",\n    \"build\": \"run-p type-check \\\"build-only {@}\\\" --\",\n    \"build-only\": \"vite build\",\n    \"type-check\": \"vue-tsc --build\",\n    \"lint\": \"run-s lint:*\",\n    \"lint:oxlint\": \"oxlint . --fix\",\n    \"lint:eslint\": \"eslint . --fix --cache\",\n    \"format\": \"oxfmt src/\"\n  },\n  \"dependencies\": {\n    \"@codemirror/autocomplete\": \"^6.20.1\",\n    \"@codemirror/commands\": \"^6.10.2\",\n    \"@codemirror/lang-javascript\": \"^6.2.5\",\n    \"@codemirror/lang-json\": \"^6.0.2\",\n    \"@codemirror/lang-yaml\": \"^6.1.2\",\n    \"@codemirror/lint\": \"^6.9.5\",\n    \"@codemirror/merge\": \"^6.12.0\",\n    \"@codemirror/state\": \"^6.5.4\",\n    \"@codemirror/theme-one-dark\": \"^6.1.3\",\n    \"@codemirror/view\": \"^6.39.16\",\n    \"codemirror\": \"^6.0.2\",\n    \"croner\": \"10.0.1\",\n    \"marked\": \"^17.0.4\",\n    \"pinia\": \"^3.0.4\",\n    \"prettier\": \"^3.8.1\",\n    \"vue\": \"^3.5.29\",\n    \"vue-draggable-plus\": \"^0.6.1\",\n    \"vue-i18n\": \"^11.2.8\",\n    \"vue-router\": \"^5.0.3\",\n    \"yaml\": \"^2.8.2\"\n  },\n  \"devDependencies\": {\n    \"@tsconfig/node24\": \"^24.0.4\",\n    \"@types/node\": \"^25.3.5\",\n    \"@vitejs/plugin-vue\": \"^6.0.4\",\n    \"@vue/eslint-config-typescript\": \"^14.7.0\",\n    \"@vue/tsconfig\": \"^0.9.0\",\n    \"eslint\": \"^10.0.3\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-plugin-oxlint\": \"^1.51.0\",\n    \"eslint-plugin-vue\": \"^10.8.0\",\n    \"less\": \"^4.5.1\",\n    \"npm-run-all2\": \"^8.0.4\",\n    \"oxfmt\": \"^0.36.0\",\n    \"oxlint\": \"^1.51.0\",\n    \"typescript\": \"~5.9.3\",\n    \"vite\": \"8.0.0-beta.16\",\n    \"vue-tsc\": \"^3.2.5\"\n  }\n}\n"
  },
  {
    "path": "frontend/src/App.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nimport { EventsOn, WindowHide, IsStartup } from '@/bridge'\nimport { NavigationBar, TitleBar, SplashView, AboutView, CommandView } from '@/components'\nimport * as Stores from '@/stores'\nimport { exitApp, sampleID, sleep, message } from '@/utils'\n\nconst loading = ref(true)\nconst percent = ref(0)\nconst hasError = ref(false)\n\nconst envStore = Stores.useEnvStore()\nconst appStore = Stores.useAppStore()\nconst pluginsStore = Stores.usePluginsStore()\nconst profilesStore = Stores.useProfilesStore()\nconst rulesetsStore = Stores.useRulesetsStore()\nconst appSettings = Stores.useAppSettingsStore()\nconst kernelApiStore = Stores.useKernelApiStore()\nconst subscribesStore = Stores.useSubscribesStore()\nconst scheduledTasksStore = Stores.useScheduledTasksStore()\n\nconst handleRestartCore = async () => {\n  try {\n    await kernelApiStore.restartCore()\n  } catch (e: any) {\n    message.error(e.message || e)\n  }\n}\n\nEventsOn('onLaunchApp', async ([arg]: string[]) => {\n  if (!arg) return\n\n  const url = new URL(arg)\n  if (url.pathname.startsWith('//import-remote-profile')) {\n    const _url = url.searchParams.get('url')\n    const _name = decodeURIComponent(url.hash).slice(1) || sampleID()\n\n    if (!_url) {\n      message.error('URL missing')\n      return\n    }\n\n    try {\n      await subscribesStore.importSubscribe(_name, _url)\n      message.success('common.success')\n    } catch (e: any) {\n      message.error(e.message || e)\n    }\n  }\n})\n\nEventsOn('onBeforeExitApp', async () => {\n  if (appSettings.app.exitOnClose) {\n    exitApp()\n  } else {\n    WindowHide()\n  }\n})\n\nEventsOn('onExitApp', () => exitApp())\n\nwindow.addEventListener('keydown', (e) => {\n  if (e.key === 'Escape') {\n    const closeFn = appStore.modalStack.at(-1)\n    closeFn?.()\n  }\n})\n\nenvStore.setupEnv().then(async () => {\n  const showError = (err: string) => {\n    hasError.value = true\n    message.error(err)\n  }\n\n  await Promise.all([\n    appSettings.setupAppSettings(),\n    profilesStore.setupProfiles(),\n    subscribesStore.setupSubscribes(),\n    rulesetsStore.setupRulesets(),\n    pluginsStore.setupPlugins(),\n    scheduledTasksStore.setupScheduledTasks(),\n  ])\n\n  const startTime = performance.now()\n  percent.value = 20\n  if (await IsStartup()) {\n    await pluginsStore.onStartupTrigger().catch(showError)\n  }\n\n  percent.value = 40\n  await pluginsStore.onReadyTrigger().catch(showError)\n\n  const duration = performance.now() - startTime\n  percent.value = duration < 500 ? 80 : 100\n\n  await sleep(Math.max(0, 1000 - duration))\n\n  loading.value = false\n  kernelApiStore.initCoreState()\n})\n</script>\n\n<template>\n  <SplashView v-if=\"loading\">\n    <Progress\n      :percent=\"percent\"\n      :status=\"hasError ? 'danger' : 'primary'\"\n      :radius=\"10\"\n      type=\"circle\"\n    />\n  </SplashView>\n  <template v-else>\n    <TitleBar />\n    <div class=\"flex-1 overflow-y-auto flex flex-col p-8\">\n      <NavigationBar />\n      <div class=\"flex flex-col overflow-y-auto mt-8 px-8 h-full\">\n        <RouterView #=\"{ Component }\">\n          <KeepAlive>\n            <component :is=\"Component\" />\n          </KeepAlive>\n        </RouterView>\n      </div>\n    </div>\n  </template>\n\n  <Modal\n    v-model:open=\"appStore.showAbout\"\n    :cancel=\"false\"\n    :submit=\"false\"\n    mask-closable\n    min-width=\"50\"\n  >\n    <AboutView />\n  </Modal>\n\n  <Menu\n    v-model=\"appStore.menuShow\"\n    :position=\"appStore.menuPosition\"\n    :menu-list=\"appStore.menuList\"\n  />\n\n  <Tips\n    v-model=\"appStore.tipsShow\"\n    :position=\"appStore.tipsPosition\"\n    :message=\"appStore.tipsMessage\"\n  />\n\n  <CommandView v-if=\"!loading\" />\n\n  <div\n    v-if=\"kernelApiStore.needRestart || kernelApiStore.restarting\"\n    class=\"fixed right-32 bottom-32\"\n  >\n    <Button\n      v-tips=\"'home.overview.restart'\"\n      :loading=\"kernelApiStore.restarting\"\n      icon=\"restart\"\n      class=\"rounded-full w-42 h-42 shadow\"\n      @click=\"handleRestartCore\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/api/kernel.ts",
    "content": "import { Request } from '@/api/request'\nimport { WebSockets } from '@/api/websocket'\nimport { useProfilesStore } from '@/stores'\n\nimport type {\n  CoreApiConfig,\n  CoreApiProxies,\n  CoreApiConnections,\n  CoreApiWsDataMap,\n} from '@/types/kernel'\n\ntype WsKey = keyof CoreApiWsDataMap\ntype WsChannel<K extends WsKey> = {\n  url: string\n  params?: Recordable\n  handlers: Array<(data: CoreApiWsDataMap[K]) => void>\n  isActive: boolean\n  connect?: () => void\n  disconnect?: () => void\n}\n\nexport enum Api {\n  Configs = '/configs',\n  Memory = '/memory',\n  Proxies = '/proxies',\n  ProxyDelay = '/proxies/{0}/delay',\n  Connections = '/connections',\n  Traffic = '/traffic',\n  Logs = '/logs',\n}\n\nconst setupCoreApi = (protocol: 'http' | 'ws') => {\n  const { currentProfile: profile } = useProfilesStore()\n\n  let base = `${protocol}://127.0.0.1:20123`\n  let bearer = ''\n\n  if (profile) {\n    const controller = profile.experimental.clash_api.external_controller || '127.0.0.1:20123'\n    const [, port = 20123] = controller.split(':')\n    base = `${protocol}://127.0.0.1:${port}`\n    bearer = profile.experimental.clash_api.secret\n  }\n\n  if (protocol === 'http') {\n    request.base = base\n    request.bearer = bearer\n  } else {\n    websocket.base = base\n    websocket.bearer = bearer\n  }\n}\n\nconst request = new Request({ beforeRequest: () => setupCoreApi('http'), timeout: 60 * 1000 })\nconst websocket = new WebSockets({ beforeConnect: () => setupCoreApi('ws') })\n\nconst wsChannels: {\n  [K in WsKey]: WsChannel<K>\n} = {\n  logs: { url: Api.Logs, isActive: false, handlers: [], params: { level: 'debug' } },\n  memory: { url: Api.Memory, isActive: false, handlers: [] },\n  traffic: { url: Api.Traffic, isActive: false, handlers: [] },\n  connections: { url: Api.Connections, isActive: false, handlers: [] },\n}\n\nconst createCoreWSHandlerRegister = <K extends WsKey>(key: K) => {\n  const channel = wsChannels[key]\n\n  return (cb: (data: CoreApiWsDataMap[K]) => void) => {\n    channel.handlers.push(cb)\n\n    if (!channel.isActive && channel.connect) {\n      channel.connect()\n      channel.isActive = true\n    }\n\n    const unregister = () => {\n      const idx = channel.handlers.indexOf(cb)\n      idx !== -1 && channel.handlers.splice(idx, 1)\n      if (channel.isActive && channel.disconnect && channel.handlers.length === 0) {\n        channel.disconnect()\n        channel.isActive = false\n      }\n    }\n    return unregister\n  }\n}\n\n// restful api\nexport const getConfigs = () => request.get<CoreApiConfig>(Api.Configs)\nexport const setConfigs = (body = {}) => request.patch<null>(Api.Configs, body)\nexport const getProxies = () => request.get<CoreApiProxies>(Api.Proxies)\nexport const getConnections = () => request.get<CoreApiConnections>(Api.Connections)\nexport const deleteConnection = (id: string) => request.delete<null>(Api.Connections + '/' + id)\nexport const useProxy = (group: string, proxy: string) => {\n  return request.put<null>(Api.Proxies + '/' + group, { name: proxy })\n}\nexport const getProxyDelay = (proxy: string, url: string, timeout: number) => {\n  return request.get<Record<string, number>>(Api.ProxyDelay.replace('{0}', proxy), {\n    url,\n    timeout,\n  })\n}\n\n// websocket api\nexport const onLogs = createCoreWSHandlerRegister('logs')\nexport const onMemory = createCoreWSHandlerRegister('memory')\nexport const onTraffic = createCoreWSHandlerRegister('traffic')\nexport const onConnections = createCoreWSHandlerRegister('connections')\nexport const initWebsocket = () => {\n  Object.values(wsChannels).forEach((channel) => {\n    const { connect, disconnect } = websocket.createWS({\n      url: channel.url,\n      params: channel.params,\n      cb: (data) => channel.handlers.forEach((cb) => cb(data)),\n    })\n    channel.connect = connect\n    channel.disconnect = disconnect\n    channel.isActive = false\n    if (channel.handlers.length > 0) {\n      channel.connect()\n      channel.isActive = true\n    }\n  })\n}\nexport const destroyWebsocket = () => {\n  Object.values(wsChannels).forEach((channel) => {\n    channel.disconnect?.()\n    channel.connect = undefined\n    channel.disconnect = undefined\n    channel.isActive = false\n  })\n}\n"
  },
  {
    "path": "frontend/src/api/request.ts",
    "content": "import { parse } from 'yaml'\n\ntype Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n\nenum ResponseType {\n  JSON = 'JSON',\n  TEXT = 'TEXT',\n  YAML = 'YAML',\n}\n\ntype RequestOptions = {\n  base?: string\n  bearer?: string\n  timeout?: number\n  responseType?: ResponseType\n  beforeRequest?: () => void\n}\n\nexport class Request {\n  public base: string\n  public bearer: string\n  public timeout: number\n  public responseType: string\n  public beforeRequest: () => void\n\n  constructor(options: RequestOptions = {}) {\n    this.base = options.base || ''\n    this.bearer = options.bearer || ''\n    this.timeout = options.timeout || 10000\n    this.responseType = options.responseType || ResponseType.JSON\n    this.beforeRequest = options.beforeRequest || (() => 0)\n  }\n\n  private request = async <T>(\n    url: string,\n    options: { method: Method; body?: Record<string, any> },\n  ) => {\n    this.beforeRequest()\n\n    const init: RequestInit = {\n      method: options.method,\n      signal: AbortSignal.timeout(this.timeout),\n    }\n\n    if (this.base) {\n      url = this.base + url\n    }\n\n    if (this.bearer) {\n      if (!init.headers) init.headers = {}\n      Object.assign(init.headers, { Authorization: `Bearer ${this.bearer}` })\n    }\n\n    if (['GET'].includes(options.method)) {\n      const query = new URLSearchParams(options.body || {}).toString()\n      query && (url += '?' + query)\n    }\n\n    if (['POST', 'PUT', 'PATCH'].includes(options.method)) {\n      init.body = JSON.stringify(options.body || {})\n    }\n\n    const res = await fetch(url, init)\n\n    if (res.status === 204) {\n      return null as T\n    }\n\n    if ([504, 401, 503].includes(res.status)) {\n      const { message } = await res.json()\n      throw message\n    }\n\n    if (this.responseType === ResponseType.TEXT) {\n      const text = await res.text()\n      return text as T\n    }\n\n    if (this.responseType === ResponseType.YAML) {\n      const text = await res.text()\n      return parse(text) as T\n    }\n\n    const json = await res.json()\n    return json as T\n  }\n\n  public get = <T>(url: string, body = {}) => this.request<T>(url, { method: 'GET', body })\n  public post = <T>(url: string, body = {}) => this.request<T>(url, { method: 'POST', body })\n  public put = <T>(url: string, body = {}) => this.request<T>(url, { method: 'PUT', body })\n  public patch = <T>(url: string, body = {}) => this.request<T>(url, { method: 'PATCH', body })\n  public delete = <T>(url: string) => this.request<T>(url, { method: 'DELETE' })\n}\n"
  },
  {
    "path": "frontend/src/api/websocket.ts",
    "content": "type WebSocketsOptions = {\n  base?: string\n  bearer?: string\n  beforeConnect?: () => void\n}\n\ntype Options = { url: string; cb: (data: any) => void; params?: Record<string, any> }\n\nexport class WebSockets {\n  public base: string\n  public bearer: string\n  public beforeConnect: () => void\n\n  constructor(options: WebSocketsOptions) {\n    this.base = options.base || ''\n    this.bearer = options.bearer || ''\n    this.beforeConnect = options.beforeConnect || (() => 0)\n  }\n\n  public createWS(options: Options) {\n    this.beforeConnect()\n\n    const params = { ...options.params, token: this.bearer }\n    const query = new URLSearchParams(params).toString()\n    const url = query ? `${options.url}?${query}` : options.url\n\n    let isManualClose = false\n    let ws: WebSocket | null = null\n\n    const connect = () => {\n      ws = new WebSocket(this.base + url)\n      ws.onmessage = (e) => options.cb(JSON.parse(e.data))\n      ws.onclose = () => {\n        setTimeout(() => {\n          if (!isManualClose) {\n            setTimeout(connect, 3000)\n          }\n        }, 1000)\n      }\n    }\n\n    const disconnect = () => {\n      isManualClose = true\n      if (ws) {\n        ws.onmessage = null\n        ws.onclose = null\n        ws.close()\n        ws = null\n      }\n    }\n\n    return { connect, disconnect }\n  }\n}\n"
  },
  {
    "path": "frontend/src/assets/globalMethods.ts",
    "content": "import * as Vue from 'vue'\nimport { stringify, parse } from 'yaml'\n\nimport * as Bridge from '@/bridge'\nimport * as Stores from '@/stores'\nimport * as Utils from '@/utils'\n\n/**\n * Expose methods to be used by the plugin system\n */\nwindow.Plugins = {\n  ...Bridge,\n  ...Utils,\n  ...Stores,\n  YAML: {\n    parse,\n    stringify,\n  },\n}\n\nwindow.Vue = Vue\n\nwindow.AsyncFunction = Object.getPrototypeOf(async function () {}).constructor\n"
  },
  {
    "path": "frontend/src/assets/logo.ts",
    "content": "export default `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACKCAYAAABipUKtAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAABMrSURBVHhe7Z0JVBRX1sddRmM0jhNHk3GS0Uy2yWYSRRZXBKEBge6mF8QVFMWNbgRUaDBBRY3mMxqXGLfBJRojJsYd950oSnAh4o57HLNMvpnvTJKJCe+7t+jWFm9DN/26qeque87veDynl+r7/99br169V9TzhkjKyWmqNWbG6wymfJ3RdElrMP0E/Ab//wH+/VJrzFoQm5IVptfrG5rfIoeHRH1dStYgrSHrHzpjFqsRg+mCNtmkMb9XDilHnNH0JFT7BlLomlkbMyL9CfNHySG1gErWQVv/hhDWEe7oUrL15o+UQwqhS81pCcKvIsSsNTBG+AQM1dr8FXKINbTJWREg1k1KRKfBMQR0FfNXySGmUI4b1xyEXwTn+wpSPI7AYHKN3A1EFCBGIFTnZUosV4FXFNqUCfKVQl2GLjX1Ua3R9B6c73+lRHIDFWC81X3T0lqZD0kOd0WMMdMXhD9DiOJ2wIS34d8Y86HJ4crQ63Mag/CTgF+qClHHVOCVh3p05h/NhyoH74g1ml6DJBcTyRcPBtPXsSkmtfmQ5XA2GGP1F6/bOmzKgpVwrjf9RCZdfODY4CO5GzgZCz8raLNy0+4tx766yC7f+oat2LCD9R87kUq4WLkFVwsq88+Rw5FYsq5As72w+JtLN++wa//4/h5ohsxZC6lkixUYG5hW4uyk+afJUV0sys9vsX73F3knzl+psBbemvJb37LVW/awgeMnUwkXJWCCW7rRmUrzz5SDik92HOhReKLs8pWvvyWFr0px2WU24f0lTJ+STSZdhFRoDaYVutRUuRtYx7Zt2x45UFw6vezKrbuU0NVRDmb5pGA/i8/MpRIuSvBehcaQFW3++d4dW/cfb19yrrzE3qq3RcnZcpYzL0863cBgwrHBsn4jMx83p8K7Ai7vGuwtOp0Gg7ofy8pvsauEqI6C3WDdjoNssGkqnXRxckNrzIwyp8U7Yteh4raHT5zdg+dwCyfPX2WXbn5DCusoJ85dYZM+WCatbmDIylOl5PzBnCLPjX3HTg8o+uriP63FtwbGAezqbVpYR8Bu8NmuQ2xI1jQ66aLEdCPWmNnbnCrPivW7iv5YePLs2uNnLpHCW4Pd4DKnboCflbtgBYuVzJUCrmcw/d2jusHe4tKwotILNymxq+MsdgNCVEfBAeb63YUsMfsdIuHiBAaI13F1kzmF0oxNxcVNC0+dmwdV/xslsD0I3eAWn25w6sJVNnXhShY7ZgKZdNEhjA0yl+ozMlqYUyqd2F9S6nu09MJZStTagGMDSlRHufL1d2zD3i/Y0AkS6gaGrGs6Q2a4ObXij91FJxKOnbn0CyWkM/DsBqcvXmNTF30kqW4A/y7ub8j5vTnN4gvcUqVLNqXHjsn+ad7q9cyeAV9t4NYNbn/HNu47At1gOp10EYLdQGvIVphTLp5QG7KfA5cesD7Y1Hfmsj1Fp0gRnYVvN7jO3lm8SjrdoPKewiJxdAPG6kPVD4eD+neVgxTA+/fLN+wgReQBXilQojoKdoNN+46yYW9JpxvA5eJVINSshPsjxjD+abhcKaAP7kEmzFnCDpWcIUV0Fp7zBqWXrrPpS1azPlK6UjBmLQw3GNzaDerrDNn9oQ19Tx6UDRJMU1j+9gOkiDwQZhEJUR3lKnSDzfuLWNJbM8jfIUZgbHAlNjk7xKyP6wJ3y8IXrqt6APaC8/PTYPR95NR5UkRncUU3kMrYAAoSn3XwIe6SMsvFNzTJJjU4zb499jWQ9PYMOOceIUXkAc9usOWAxLqB0VQO9DLL5nzgvDQ4azl8ONd9d1hZs1euE9b5USI6C99ucIPNWPqxpLqB1pi5wOlugKNMYSaK+BJeGKbMZjsKvyRF5AHXsYHEuoGwZzI5O9gsp/0xID29GXzAfOG8Qn0wZ/qmvc0Wrt3ssskjnt3gKxwbSKwbwL/z9aNyHjPLW33EJpu6whvOW3+Iuxj77gds77HTpIg84D02GCaxbqAxmILMMj8cSUmLGmkNmTPgfH+X/AA3MWDcJLZi405SQB5w7QaXb8CVgtTGBqYpOIFnlr0y9Pr8hlpj1kbqTXUFTh4ddNHkESJ0A6hkSlhHuN8NpDSLmD3eLH1lwGAvhX5h3YKTR2u37ycF5AF2g6q7j2pLZTeQyryB6T8P7F2EkX6dnPPtASePcCHHF6fOkSLyoOzKTa7dQAp3GM3PQqw8FVAvEBs44MLFHJSAPODdDYQ7jCJeixiZkJwM0j8qmCBm1DjyRWID2+t7y9eyo6UXSBF5wLMbbN5/VLRrEf0Vyqkg/p+BBvUU/YeRLxIrybmz2LZDxaSAPDjB+UphGnQDMe1TUA1PY+27Bs8F8Z8RDBAQEcOUSWnki8VKXNrbbMGajS6bSkaEXUscu4EY9inEjBzHsODbdwmad88A/mEq1rm3hkUOMZBvEjNp0+ez3UdPkgLyoHJswO8O4xQY0NZVN4gelsJC+w0VeMgAFoJjE5gmOZP8ALEyYNxElrd+u8umkhGes4gbYTDrzj2MmtEZLHzQyHviV2sApGuUnkUPHUN+mJjJmr2Y7S/+ihSQBzyvFLAbTF6wnOmJ38ETZVKq0PKtxa/RAJWoWUjfRJw+JD9YrCRkTmGrt+5hxwkBecHzSuHzPYXCMVO/xRm00MUjEkY/JLwFOwxQSTdlH2HUSH2JWMFz7KQPlrPDJ8pIAXnA854C7lOYOD+P/C21QTU8nSkGJJHCW7DbAEhAuFr4QJ1RWt0AZ+Q+3XmIFJAXlfcUaGEdAbsB7mgelFH7Zx9ht44cnEwKXhWHDGChh7ovU48cS365WMHJI1zN48qpZKEbcNzDiDfCqN9SHahL2MDhpNgUtTIAgnMGOKKkDkLMjJw4U1j7TwnIC+wGlKiOgt1gbcE+4dY49VseAKo+KtFIilwdtTaAhZ6aASxm1Hj6oEQKTh7N+egzl04l8+wGJefKWeZ7tp+LiPkPHziCFLgmnDYA0rm3lvWGcw51cGImZdocVnDYdesQEV67lrAbrNq8i/VLz3ngN+BleihxeWcvXAxgIVgfL0w2WB+g2MFta4vyN7MiF04ln4TzOa9uUFx2iWXMXCDkOSL+wUmd2sDVAEiXKB2LGppCJlvMjJ/5oUunkr88C93g6tekqI6CTztJnTyTFNRRuBvAQkjcEGESgkq2WBmUkStsYnXljaVTF64Jj7mlhLWHcugk2FFm/f1jUlBHcZkBkG7RscIUJJVssYKTR2/NXcr2Hy8lBeTBl2fL2bladIPz124L78XPmJ0nAQMgwuQRrjOQ2FQy3rL9eOtel95YOnURuoEdT0fFbet4z8D6vZIxgIXu6jimHpFOJlus4OTRlIUfuWxLO4IVjZVNCY9cvHEHLgOvPPQ+yRkAESaP4HqVSraYGZ7zLvt012GXdoPSi9eFwZ1FeLzlXFZ+k3wtIkkDWAjU9GdSWX9oASePZuatZYUnz5KC8AANho+4tQz0qNdYkLQBEFx51DthNJlsMWPIne2SqeR9MOhcvmEnW7xuG9ty4Bj5GmskbwALQfpBkpw8mrf6cy4PuMCq37j3KAi/lS3Kr2TJpwU1nm48xgBIl0idcCODSraYSZ8xnxUcrv2q5EMlZWzlpt33hLeAXaCmmUmPMoCFXn0GS2/yaPxkoXodubGE1b314HG2BISuKr5XGwDpGq1nymHSWoeIk0e4DtGeqeTCk+fYx1v3kcJb8GoDCISrhYOT2jrEwVlT2YqNu2xOJe8oLGFLPysgRbdGNoCZ7qo4YV0blWyxgpNHuA7Reir56OkLLH/7QVJsCtkAVuBUMi5xopItZvAJaPgMYnxMbt76HaTQtpANQNAjph9Tj5TW5NHgjFxS4JqQDWCDzhEaFpEwiky22IiGgSyu1Jm/egMpcnXYY4D3l60hBXUUSRnAQk/dQBYzWpzrECtX6oy6l2BXGWB2nhcbAOkSqRXdJlbl8LSHtl+5ygDvLlr5wPfUFskawAJuYq3rySP8/t6D6e1XrjKA6d155Pc5iuQNgAibWOto8kg9Ymy1269cYYBjZy6x2FFjye9zFI8wgAV3bmIVtl/BKYhKqjWuMMDS/E3kd9UGjzIA4o5NrPh0DXu3X/E2wPbDx5kqsWbj2YvHGQC5v4mVFrD2mIQl71QibcHTAFv2H2G6Eank99QWjzSABZ6bWDVw2Rk+yPHtV7wMsGjN5ywy4f7lJS882gCIsA4x3rlNrHh3knq6hj04a4ADxaUsNZfPJhAKjzeAhZ5axzexapIfnNSpDc4YYPXmnUyTNIb8XF54jQEQnErGSrZn3sDWM3UcxVEDLFy7hY2dNke4zufx/TXhVQawYM9updqc7ynmrfqcFJpi+uLV3K7v7UU2gA14GWDWsnxSbGsWrNnEknNmuKXiqyIbwAa8DDBx7lJSdAu5HyxnMVYPbnQ3sgFswMsAiRmTSOHx1DDMlMsUxHvciWwAG/AyALb17FkL7wsPg0Ic5EXZuHnkbqoYQPlfKmGehjsNYAHbvG5EOgur4bl97ua1gMD37xtAobxNJczTqAsDiJUXOwbkgvhtBQP4KVSHqIR5GrIB7vOnds+PBPHxD0bUr+cXppxJJczTkA1QSa8+Cf/XsGHDKBC/8g9H+YWpulMJ8zRkA1QSEBFzEGTvDjQTDABRH04DJVTSPAnZAEhixV9eeDkLNH8F+J2gPoZvqLKHn0L5K5U4T0E2wFDcfofVHwa0Ah78C6K+CpUBEvVb1cR5Ct5ugO7quAtNmjXrD1L/DWgkiF414FSghk5wlUqg1PFWA4T0HXIXOvzOJk2a9QWJXweaCmLbCp+kpEZwZTAO+JFKpFTxRgN0VfY53bLN0waQNQR4AUDxH2z9tsI3LPpVuEI4SiVTiniTAXrFDf7x1S498+ByTw1SYtW3BO4P+uwNHx8f6AZqE5wafqaSKiW8xQDdVX3OtGrT1gjy9QRwtq8JYF/V24qACFV7/zBlMZVYqeDpBgiJG/JT+y7BK6DqY0CyN4DHgYaoH5fw8YGxgUL5tr9CmjePPNkAMMI/98RT7caATEFAO8D5qrcVnXopO0A3OEElWcx4ogF6xQ35+fVuwaug6vHPwb8J8K16W/GKXt8YusFk6Aa/UMkWI55mgO7qfhee+Mtf00AOrHq8teu6qrcVviHKTnC5eJpKuNjwFAPAuf6XN3qErIGq14IEHQAc4bu+6m1FYGBgE3+FahpcMt6lEi8WPMEAPWL6Xn7ymWfHQdp7AVj1jwLurXpbEaBQ+4ERzlDJFwNSNgBWfYdARX7Dho11kOqOAN7Kdfy63tURGBjfxD9U+T9ivLEkVQP00PS/0ubZ5zMgvVj1fwXEU/W2orNC1QW6wTlKiLpCagYI6Zt4t0PPsPUNGzfWQ0p9AHFWva3wiYpqCpeLs+FKQRR3GKVkgEBN/+tPPfeSCdKIc/jPAvbP4YsthLUGYaqLlCjuRAoGgKr/tWNwxMZGjRr1gdR1AvC+vXSq3laEhoY2g24wry67gdgNEKgZcPPpF16egOkCngOkW/W2AgaHQSBGeVVx3IFYDRDSD6s+ckujJk3iIEW+QGtA+lVvK7oolc1hgPihu7uBGA3QUzvwVtuXXsuBtCiA5wHPq3pb4a+IDnXn6iMxGQDO9b91CoksgKrvB6nwAzy76m2FT4i+BZhgCQhUUVUw3ojFAEHagbfbvdx+Evx8XJyJq3RwebZ3VL2t8A9VR8Ap4QYlHC/q2gBY9b4h0TvMCzOx6p8A6MWZ3hjdIiMfh26wDMRySTeoSwP01A2688wrr0+BnxkOyFVfXQSEqqL9wlS3KBGdoS4MAFVfAabe3aRp84Hw0/yBJwG56muKAIWuJZhgFQjHrRu42wBBukHfPvtqh2nwc7DqcS3+Y0ADQA57wzdcGQNjAy7b191mAKz6cOW+ps2bD4KfEADIVe9MvBke3tovTPkJJaojuMMAQfr47557veMMOOwIQK56ngFVpQcj3KHEtQfXGiCxwj9cfaBp8xYJcKidgT8BjfG45eAY/sHRT4IJPqUErglXGQCq/p/Pv9FpJhxeb+AloDkgV70Lo75fWHRff4XqW0poW7jAAFD1MYebt2g5GI6pC9AGkKveXdExLKYNDBA3UGJT8DRAcGzCDy+86TsLDiMSeBmQq76OArqBaiCcFr6nRLeGlwECemu+eKxly0T4brnqxRIBkZFPwSlhCyW8BWcNEBQb/78vdvDDx6vh83XwCRty1Yss6sMpYbB/mPIH3gbo3Ft75LGWrbDquwL4dC2senkqV4zhExLVFoywnYcB4Fz/rxc7+s+Fj7VU/e8BueolEA38FdFJcFr41z0D2PEn5sIG3P+jUFD1RS0ebzUMPqsbIFe9FAOEf8ZPodqFBqjpr5Hjn3+trPr4f/+tU2d8lm40IFe99COnARggoXOk9lp1fz4mIn7UXf9w9V6o+qHwJqz6p4BHALnqPSHw+QahcUODtcasdTqj6Wez8BUxI8ed9wtXzflD69bx8LJgAOfw5ar35Ag3zHlEMyq7nXr0aNxtg2JjteN5vgUg37nzwsA274Wtvl69/wfOcO4gRpbr1QAAAABJRU5ErkJggg==`\n"
  },
  {
    "path": "frontend/src/assets/main.less",
    "content": "@import 'styles/variables.less';\n@import 'styles/theme.less';\n@import 'styles/reset.less';\n@import 'styles/custom.less';\n@import 'styles/utilities/index.less';\n"
  },
  {
    "path": "frontend/src/assets/polyfills.ts",
    "content": "// Polyfill for Promise.withResolvers()\nif (typeof Promise.withResolvers !== 'function') {\n  Promise.withResolvers = function <T>() {\n    let resolve!: (value: T | PromiseLike<T>) => void\n    let reject!: (reason?: any) => void\n\n    const promise = new Promise<T>((res, rej) => {\n      resolve = res\n      reject = rej\n    })\n\n    return { promise, resolve, reject }\n  }\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/custom.less",
    "content": "body[feature-outline='true'] {\n  * {\n    outline: 1px solid var(--color);\n  }\n}\n\nbody[feature-no-animation='true'] {\n  * {\n    transition: none !important;\n  }\n}\n\nbody[feature-no-rounded='true'] {\n  .rounded-2,\n  .rounded-4,\n  .rounded-6,\n  .rounded-8,\n  .rounded-16,\n  .rounded-32,\n  .rounded-9999,\n  .rounded-full {\n    border-radius: 0;\n  }\n  ::-webkit-scrollbar-track {\n    border-radius: 0;\n  }\n  ::-webkit-scrollbar-thumb {\n    border-radius: 0;\n  }\n}\n\nbody[feature-border='true'] {\n  box-shadow: inset 0 0 1px var(--color);\n}\n\nbody {\n  margin: 0;\n  color: var(--color);\n  background-color: var(--bg-color);\n  user-select: none;\n  -webkit-user-select: none;\n  line-height: 1.32;\n}\n\n#app {\n  height: 100vh;\n  display: flex;\n  flex-direction: column;\n}\n\n.grid-list-grid {\n  flex: 1;\n  margin-top: 8px;\n  overflow-y: auto;\n  font-size: 12px;\n  .grid-list-item {\n    display: inline-block;\n    margin: 8px;\n    width: calc(100% / 3 - 16px);\n  }\n}\n\n.grid-list-list {\n  flex: 1;\n  margin-top: 8px;\n  overflow-y: auto;\n  font-size: 12px;\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  padding: 8px;\n}\n\n.grid-list-header {\n  display: flex;\n  align-items: center;\n  padding: 0 8px;\n}\n\n.grid-list-empty {\n  height: 90%;\n}\n\n.form-item {\n  font-size: 14px;\n  padding: 4px 12px;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.form-action {\n  display: flex;\n  justify-content: flex-end;\n  margin-top: 8px;\n}\n\n.rotation {\n  animation: rotate 1s infinite linear;\n}\n\n.hover\\:\\!bg-red {\n  &:hover {\n    background-color: rgba(255, 0, 0, 0.6) !important;\n  }\n}\n\n@keyframes clip {\n  from {\n    clip-path: circle(0% at var(--x) var(--y));\n  }\n  to {\n    clip-path: circle(100% at var(--x) var(--y));\n  }\n}\n\n@keyframes rotate {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.slide-down-enter-active,\n.slide-down-leave-active {\n  transition: transform 0.2s ease-out;\n}\n\n.slide-down-enter-from,\n.slide-down-leave-to {\n  transform: translateY(-100%);\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/reset.less",
    "content": "div,\ninput {\n  box-sizing: border-box;\n}\n\ninput {\n  font-family: inherit;\n}\n\na {\n  text-decoration: none;\n  color: var(--color);\n}\n\n::view-transition-old(root) {\n  animation: none;\n}\n::view-transition-new(root) {\n  mix-blend-mode: normal;\n  animation: clip 0.5s ease-in;\n}\n\n::-webkit-scrollbar {\n  width: 6px;\n  height: 6px;\n}\n::-webkit-scrollbar-track {\n  border-radius: 6px;\n  background: var(--scrollbar-track-bg);\n}\n::-webkit-scrollbar-thumb {\n  border-radius: 6px;\n  background: var(--scrollbar-thumb-bg);\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/theme.less",
    "content": ":root {\n  body[theme-mode='light'] {\n    .set-theme(light);\n  }\n\n  body[theme-mode='dark'] {\n    .set-theme(dark);\n  }\n}\n\n.set-theme(@theme) {\n  --color: ~'var(--color-@{theme})';\n  --bg-color: ~'var(--bg-color-@{theme})';\n\n  // Scrollbar\n  --scrollbar-track-bg: ~'var(--scrollbar-track-bg-@{theme})';\n  --scrollbar-thumb-bg: ~'var(--scrollbar-thumb-bg-@{theme})';\n\n  // Button\n  --btn-normal-color: ~'var(--btn-normal-color-@{theme})';\n  --btn-normal-bg: ~'var(--btn-normal-bg-@{theme})';\n  --btn-normal-hover-color: ~'var(--btn-normal-hover-color-@{theme})';\n  --btn-normal-hover-border-color: ~'var(--btn-normal-hover-border-color-@{theme})';\n  --btn-normal-active-color: ~'var(--btn-normal-active-color-@{theme})';\n  --btn-normal-active-border-color: ~'var(--btn-normal-active-border-color-@{theme})';\n\n  --btn-primary-color: ~'var(--btn-primary-color-@{theme})';\n  --btn-primary-bg: ~'var(--btn-primary-bg-@{theme})';\n  --btn-primary-hover-bg: ~'var(--btn-primary-hover-bg-@{theme})';\n  --btn-primary-active-bg: ~'var(--btn-primary-active-bg-@{theme})';\n\n  --btn-link-color: ~'var(--btn-link-color-@{theme})';\n  --btn-link-bg: ~'var(--btn-link-bg-@{theme})';\n  --btn-link-hover-color: ~'var(--btn-link-hover-color-@{theme})';\n  --btn-link-active-color: ~'var(--btn-link-active-color-@{theme})';\n\n  --btn-text-color: ~'var(--btn-text-color-@{theme})';\n  --btn-text-bg: ~'var(--btn-text-bg-@{theme})';\n  --btn-text-hover-bg: ~'var(--btn-text-hover-bg-@{theme})';\n  --btn-text-active-bg: ~'var(--btn-text-active-bg-@{theme})';\n\n  // Radio\n  --radio-normal-color: ~'var(--radio-normal-color-@{theme})';\n  --radio-normal-bg: ~'var(--radio-normal-bg-@{theme})';\n  --radio-normal-hover-color: ~'var(--radio-normal-hover-color-@{theme})';\n  --radio-primary-color: ~'var(--radio-primary-color-@{theme})';\n  --radio-primary-bg: ~'var(--radio-primary-bg-@{theme})';\n  --radio-primary-hover-bg: ~'var(--radio-primary-hover-bg-@{theme})';\n  --radio-primary-active-bg: ~'var(--radio-primary-active-bg-@{theme})';\n\n  // Card\n  --card-color: ~'var(--card-color-@{theme})';\n  --card-bg: ~'var(--card-bg-@{theme})';\n  --card-hover-bg: ~'var(--card-hover-bg-@{theme})';\n  --card-active-bg: ~'var(--card-active-bg-@{theme})';\n\n  // Progress\n  --progress-bg: ~'var(--progress-bg-@{theme})';\n  --progress-inner-bg: ~'var(--progress-inner-bg-@{theme})';\n\n  // Dropdown\n  --dropdown-bg: ~'var(--dropdown-bg-@{theme})';\n\n  // Modal\n  --modal-bg: ~'var(--modal-bg-@{theme})';\n  --modal-mask-bg: ~'var(--modal-mask-bg-@{theme})';\n\n  // Switch\n  --switch-on-bg: ~'var(--switch-on-bg-@{theme})';\n  --switch-on-hover-bg: ~'var(--switch-on-hover-bg-@{theme})';\n  --switch-on-dot-bg: ~'var(--switch-on-dot-bg-@{theme})';\n  --switch-off-bg: ~'var(--switch-off-bg-@{theme})';\n  --switch-off-hover-bg: ~'var(--switch-off-hover-bg-@{theme})';\n  --switch-off-dot-bg: ~'var(--switch-off-dot-bg-@{theme})';\n\n  // Input\n  --input-color: ~'var(--input-color-@{theme})';\n  --input-bg: ~'var(--input-bg-@{theme})';\n\n  // ColorPicker\n  --color-picker-bg: ~'var(--color-picker-bg-@{theme})';\n\n  // Divider\n  --divider-color: ~'var(--divider-color-@{theme})';\n\n  // Select\n  --select-bg: ~'var(--select-bg-@{theme})';\n\n  // Toast\n  --toast-bg: ~'var(--toast-bg-@{theme})';\n\n  // Menu\n  --menu-bg: ~'var(--menu-bg-@{theme})';\n\n  // Table\n  --table-tr-odd-bg: ~'var(--table-tr-odd-bg-@{theme})';\n  --table-tr-even-bg: ~'var(--table-tr-even-bg-@{theme})';\n  --table-tr-odd-hover-bg: ~'var(--table-tr-odd-hover-bg-@{theme})';\n  --table-tr-even-hover-bg: ~'var(--table-tr-even-hover-bg-@{theme})';\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/utilities/display.less",
    "content": ".block {\n  display: block;\n}\n\n.inline-block {\n  display: inline-block;\n}\n\n.inline {\n  display: inline;\n}\n\n.flex {\n  display: flex;\n}\n\n.inline-flex {\n  display: inline-flex;\n}\n\n.grid {\n  display: grid;\n}\n\n.hidden {\n  display: none;\n}\n\n.\\!hidden {\n  display: none !important;\n}\n\n.invisible {\n  visibility: hidden;\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/utilities/flex.less",
    "content": ".flex-row {\n  flex-direction: row;\n}\n.flex-row-reverse {\n  flex-direction: row-reverse;\n}\n.flex-col {\n  flex-direction: column;\n}\n.flex-col-reverse {\n  flex-direction: column-reverse;\n}\n\n.flex-wrap {\n  flex-wrap: wrap;\n}\n\n.items-start {\n  align-items: flex-start;\n}\n.items-center {\n  align-items: center;\n}\n.items-end {\n  align-items: flex-end;\n}\n.items-stretch {\n  align-items: stretch;\n}\n.self-stretch {\n  align-self: stretch;\n}\n\n.justify-start {\n  justify-content: flex-start;\n}\n.justify-center {\n  justify-content: center;\n}\n.justify-end {\n  justify-content: flex-end;\n}\n.justify-between {\n  justify-content: space-between;\n}\n.justify-around {\n  justify-content: space-around;\n}\n.justify-evenly {\n  justify-content: space-evenly;\n}\n.shrink-0 {\n  flex-shrink: 0;\n}\n.flex-1 {\n  flex: 1 1 0%;\n}\n.flex-none {\n  flex: none;\n}\n.flex-auto {\n  flex: 1 1 auto;\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/utilities/gap.less",
    "content": "@gap-values: 0, 2, 4, 8, 10, 12, 16, 24, 32;\n\n.generate-gaps(@i: 1) when (@i <= length(@gap-values)) {\n  @v: extract(@gap-values, @i);\n\n  .gap-@{v} {\n    gap: unit(@v, px);\n  }\n  .gap-x-@{v} {\n    column-gap: unit(@v, px);\n  }\n  .gap-y-@{v} {\n    row-gap: unit(@v, px);\n  }\n\n  .generate-gaps(@i + 1);\n}\n\n.generate-gaps();\n"
  },
  {
    "path": "frontend/src/assets/styles/utilities/grid.less",
    "content": "@cols: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 24, 32, 64;\n\n.generate-cols(@i: 1) when (@i <= length(@cols)) {\n  @n: extract(@cols, @i);\n\n  .grid-cols-@{n} {\n    grid-template-columns: repeat(@n, minmax(0, 1fr));\n  }\n\n  .col-span-@{n} {\n    grid-column: span @n / span @n;\n  }\n\n  .generate-cols(@i + 1);\n}\n.generate-cols();\n"
  },
  {
    "path": "frontend/src/assets/styles/utilities/index.less",
    "content": "@import 'spacing.less';\n@import 'gap.less';\n@import 'text.less';\n@import 'display.less';\n@import 'flex.less';\n@import 'grid.less';\n@import 'rounded.less';\n@import 'size.less';\n@import 'others.less';\n"
  },
  {
    "path": "frontend/src/assets/styles/utilities/others.less",
    "content": ".fixed {\n  position: fixed;\n}\n\n.sticky {\n  position: sticky;\n}\n\n.relative {\n  position: relative;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.inset-0 {\n  inset: 0;\n}\n\n.z-2 {\n  z-index: 2;\n}\n\n.z-3 {\n  z-index: 3;\n}\n\n.z-9 {\n  z-index: 9;\n}\n\n.z-99 {\n  z-index: 99;\n}\n\n.z-999 {\n  z-index: 999;\n}\n\n.z-9999 {\n  z-index: 9999;\n}\n\n.top-0 {\n  top: 0;\n}\n\n.right-8 {\n  right: 8px;\n}\n\n.right-32 {\n  right: 32px;\n}\n\n.bottom-4 {\n  bottom: 4px;\n}\n\n.bottom-12 {\n  bottom: 12px;\n}\n\n.bottom-32 {\n  bottom: 32px;\n}\n\n.left-8 {\n  left: 8px;\n}\n\n.left-1\\/2 {\n  left: 50%;\n}\n\n.backdrop-blur-sm {\n  backdrop-filter: blur(4px);\n}\n\n.blur-3xl {\n  filter: blur(64px);\n}\n\n.origin-center {\n  transform-origin: center;\n}\n\n.rotate-0 {\n  transform: rotate(0deg);\n}\n\n.-rotate-90 {\n  transform: rotate(-90deg);\n}\n\n.rotate-90 {\n  transform: rotate(90deg);\n}\n\n.rotate-180 {\n  transform: rotate(180deg);\n}\n\n.-translate-x-1\\/2 {\n  transform: translateX(-50%);\n}\n\n.translate-y-0 {\n  transform: translateY(0);\n}\n\n.translate-y-full {\n  transform: translateY(100%);\n}\n\n.cursor-pointer {\n  cursor: pointer;\n}\n\n.cursor-move {\n  cursor: move;\n}\n\n.cursor-not-allowed {\n  cursor: not-allowed;\n}\n\n.pointer-events-none {\n  pointer-events: none;\n}\n\n.overflow-auto {\n  overflow: auto;\n}\n\n.overflow-y-auto {\n  overflow-y: auto;\n}\n\n.overflow-hidden {\n  overflow: hidden;\n}\n\n.transition-all {\n  transition-property: all;\n}\n\n.duration-100 {\n  transition-duration: 0.1s;\n}\n\n.duration-200 {\n  transition-duration: 0.2s;\n}\n\n.duration-400 {\n  transition-duration: 0.4s;\n}\n\n.shadow {\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n}\n\n.outline-none {\n  outline: none;\n}\n\n.border-0 {\n  border-width: 0;\n}\n\n.bg-transparent {\n  background: transparent;\n}\n\n.border-collapse {\n  border-collapse: collapse;\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/utilities/rounded.less",
    "content": "@radius-values: 0, 2, 4, 6, 8, 16, 32, 9999;\n\n.generate-rounded(@i: 1) when (@i <= length(@radius-values)) {\n  @r: extract(@radius-values, @i);\n  @name: ~'@{r}';\n  .rounded-@{name} {\n    border-radius: unit(@r, px);\n  }\n  .generate-rounded(@i + 1);\n}\n.generate-rounded();\n\n.rounded-full {\n  border-radius: 9999px;\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/utilities/size.less",
    "content": "@size-values: 0, 8, 10, 12, 16, 18, 24, 26, 30, 32, 42, 64, 128, 256;\n@percent-values: 25, 36, 50, 60, 75, 90, 100;\n\n.generate-size(@i: 1) when (@i <= length(@size-values)) {\n  @v: extract(@size-values, @i);\n  .w-@{v} {\n    width: unit(@v, px);\n  }\n  .h-@{v} {\n    height: unit(@v, px);\n  }\n  .min-w-@{v} {\n    min-width: unit(@v, px);\n  }\n  .min-h-@{v} {\n    min-height: unit(@v, px);\n  }\n  .generate-size(@i + 1);\n}\n.generate-size();\n\n.generate-percent(@i: 1) when (@i <= length(@percent-values)) {\n  @p: extract(@percent-values, @i);\n  .w-\\[@{p}\\%\\] {\n    width: ~'@{p}%';\n  }\n  .h-\\[@{p}\\%\\] {\n    height: ~'@{p}%';\n  }\n  .min-w-\\[@{p}\\%\\] {\n    min-width: ~'@{p}%';\n  }\n  .min-h-\\[@{p}\\%\\] {\n    min-height: ~'@{p}%';\n  }\n  .max-w-\\[@{p}\\%\\] {\n    max-width: ~'@{p}%';\n  }\n  .max-h-\\[@{p}\\%\\] {\n    max-height: ~'@{p}%';\n  }\n  .generate-percent(@i + 1);\n}\n.generate-percent();\n\n.w-full {\n  width: 100%;\n}\n\n.h-full {\n  height: 100%;\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/utilities/spacing.less",
    "content": "@spacing-values: 0, 2, 4, 6, 8, 12, 16, 20, 24, 32, 36;\n@spacing-types: m, p;\n@directions: '', t, r, b, l, x, y;\n\n.generate-spacing(@typeIndex: 1) when (@typeIndex <= length(@spacing-types)) {\n  @type: extract(@spacing-types, @typeIndex);\n\n  .generate-direction(@dirIndex: 1) when (@dirIndex <= length(@directions)) {\n    @dir: extract(@directions, @dirIndex);\n\n    .generate-value(@valIndex: 1) when (@valIndex <= length(@spacing-values)) {\n      @val: extract(@spacing-values, @valIndex);\n      @class: ~'@{type}@{dir}-@{val}';\n\n      .@{class} {\n        .apply-spacing(@type, @dir, unit(@val, px));\n      }\n\n      .generate-value(@valIndex + 1);\n    }\n\n    .generate-auto() when (@type = m) {\n      @class: ~'@{type}@{dir}-auto';\n      .@{class} {\n        .apply-spacing(@type, @dir, auto);\n      }\n    }\n\n    .generate-value();\n    .generate-auto();\n    .generate-direction(@dirIndex + 1);\n  }\n\n  .generate-direction();\n  .generate-spacing(@typeIndex + 1);\n}\n\n.generate-spacing();\n\n.apply-spacing(@type, '', @value) when (@type = m) {\n  margin: @value;\n}\n.apply-spacing(@type, '', @value) when (@type = p) {\n  padding: @value;\n}\n\n.apply-spacing(@type, t, @value) when (@type = m) {\n  margin-top: @value;\n}\n.apply-spacing(@type, t, @value) when (@type = p) {\n  padding-top: @value;\n}\n\n.apply-spacing(@type, r, @value) when (@type = m) {\n  margin-right: @value;\n}\n.apply-spacing(@type, r, @value) when (@type = p) {\n  padding-right: @value;\n}\n\n.apply-spacing(@type, b, @value) when (@type = m) {\n  margin-bottom: @value;\n}\n.apply-spacing(@type, b, @value) when (@type = p) {\n  padding-bottom: @value;\n}\n\n.apply-spacing(@type, l, @value) when (@type = m) {\n  margin-left: @value;\n}\n.apply-spacing(@type, l, @value) when (@type = p) {\n  padding-left: @value;\n}\n\n.apply-spacing(@type, x, @value) when (@type = m) {\n  margin-left: @value;\n  margin-right: @value;\n}\n.apply-spacing(@type, x, @value) when (@type = p) {\n  padding-left: @value;\n  padding-right: @value;\n}\n\n.apply-spacing(@type, y, @value) when (@type = m) {\n  margin-top: @value;\n  margin-bottom: @value;\n}\n.apply-spacing(@type, y, @value) when (@type = p) {\n  padding-top: @value;\n  padding-bottom: @value;\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/utilities/text.less",
    "content": "@text-sizes: 10, 12, 14, 16, 18, 20, 24, 32;\n\n.generate-text(@i: 1) when (@i <= length(@text-sizes)) {\n  @s: extract(@text-sizes, @i);\n  .text-@{s} {\n    font-size: unit(@s, px);\n  }\n  .generate-text(@i + 1);\n}\n.generate-text();\n\n.text-left {\n  text-align: left;\n}\n.text-center {\n  text-align: center;\n}\n.text-right {\n  text-align: right;\n}\n.align-middle {\n  vertical-align: middle;\n}\n\n.line-clamp-1 {\n  display: -webkit-box;\n  -webkit-line-clamp: 1;\n  line-clamp: 1;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n}\n\n.line-clamp-2 {\n  display: -webkit-box;\n  -webkit-line-clamp: 2;\n  line-clamp: 2;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n}\n\n.underline {\n  text-decoration: underline;\n}\n\n.text-nowrap {\n  text-wrap: nowrap;\n}\n\n.text-ellipsis {\n  text-overflow: ellipsis;\n}\n\n.break-all {\n  word-break: break-all;\n}\n\n.break-keep {\n  word-break: keep-all;\n}\n\n.whitespace-nowrap {\n  white-space: nowrap;\n}\n\n.whitespace-pre-wrap {\n  white-space: pre-wrap;\n}\n\n.leading-relaxed {\n  line-height: 1.625;\n}\n\n.italic {\n  font-style: italic;\n}\n\n.font-bold {\n  font-weight: bold;\n}\n\n.font-normal {\n  font-weight: normal;\n}\n\n.select-text {\n  user-select: text;\n  -webkit-user-select: text;\n}\n"
  },
  {
    "path": "frontend/src/assets/styles/variables.less",
    "content": ":root {\n  --x: 0px;\n  --y: 0px;\n\n  --primary-color: rgb(0, 89, 214);\n  --secondary-color: rgb(5, 62, 142);\n\n  // Color & BackgroundColor\n  --color-light: #000;\n  --bg-color-light: rgba(246, 246, 246, 0.85);\n  --color-dark: #fff;\n  --bg-color-dark: rgba(0, 0, 0, 0.85);\n\n  // Scrollbar\n  --scrollbar-track-bg-light: rgb(220, 220, 220);\n  --scrollbar-thumb-bg-light: var(--primary-color);\n  --scrollbar-track-bg-dark: rgb(73, 73, 73);\n  --scrollbar-thumb-bg-dark: var(--primary-color);\n\n  // Button\n  --btn-normal-color-light: #000000;\n  --btn-normal-bg-light: rgb(255, 255, 255);\n  --btn-normal-hover-color-light: var(--primary-color);\n  --btn-normal-hover-border-color-light: var(--primary-color);\n  --btn-normal-active-color-light: var(--secondary-color);\n  --btn-normal-active-border-color-light: var(--secondary-color);\n\n  --btn-normal-color-dark: #000000;\n  --btn-normal-bg-dark: rgb(255, 255, 255);\n  --btn-normal-hover-color-dark: var(--primary-color);\n  --btn-normal-hover-border-color-dark: var(--primary-color);\n  --btn-normal-active-color-dark: var(--secondary-color);\n  --btn-normal-active-border-color-dark: var(--secondary-color);\n\n  --btn-primary-color-light: rgb(255, 255, 255);\n  --btn-primary-bg-light: var(--primary-color);\n  --btn-primary-hover-bg-light: var(--secondary-color);\n  --btn-primary-active-bg-light: var(--primary-color);\n\n  --btn-primary-color-dark: rgb(255, 255, 255);\n  --btn-primary-bg-dark: var(--primary-color);\n  --btn-primary-hover-bg-dark: var(--secondary-color);\n  --btn-primary-active-bg-dark: var(--primary-color);\n\n  --btn-link-color-light: var(--primary-color);\n  --btn-link-bg-light: transparent;\n  --btn-link-hover-color-light: var(--secondary-color);\n  --btn-link-active-color-light: var(--primary-color);\n\n  --btn-link-color-dark: var(--primary-color);\n  --btn-link-bg-dark: transparent;\n  --btn-link-hover-color-dark: var(--secondary-color);\n  --btn-link-active-color-dark: var(--primary-color);\n\n  --btn-text-color-light: rgb(46, 46, 46);\n  --btn-text-bg-light: transparent;\n  --btn-text-hover-bg-light: rgb(232, 232, 232);\n  --btn-text-active-bg-light: rgb(206, 206, 206);\n\n  --btn-text-color-dark: rgb(230, 230, 230);\n  --btn-text-bg-dark: transparent;\n  --btn-text-hoer-color-dark: #222222;\n  --btn-text-hover-bg-dark: rgba(255, 255, 255, 0.2);\n  --btn-text-active-color-dark: #161616;\n  --btn-text-active-bg-dark: rgba(255, 255, 255, 0.4);\n\n  // Radio\n  --radio-normal-color-light: #000;\n  --radio-normal-bg-light: rgba(255, 255, 255, 1);\n  --radio-normal-hover-color-light: var(--primary-color);\n  --radio-primary-color-light: #fff;\n  --radio-primary-bg-light: var(--primary-color);\n  --radio-primary-hover-bg-light: var(--secondary-color);\n  --radio-primary-active-bg-light: var(--primary-color);\n\n  --radio-normal-color-dark: #ededed;\n  --radio-normal-bg-dark: rgba(255, 255, 255, 0.06);\n  --radio-normal-hover-color-dark: var(--primary-color);\n  --radio-primary-color-dark: #fff;\n  --radio-primary-bg-dark: var(--primary-color);\n  --radio-primary-hover-bg-dark: var(--secondary-color);\n  --radio-primary-active-bg-dark: var(--primary-color);\n\n  // Card\n  --card-color-light: rgb(95, 95, 95);\n  --card-bg-light: rgba(255, 255, 255, 0.6);\n  --card-hover-bg-light: rgba(255, 255, 255, 0.6);\n  --card-active-bg-light: rgba(255, 255, 255, 0.4);\n\n  --card-color-dark: rgb(255, 255, 255);\n  --card-bg-dark: rgba(255, 255, 255, 0.06);\n  --card-hover-bg-dark: rgba(255, 255, 255, 0.1);\n  --card-active-bg-dark: rgba(255, 255, 255, 0.04);\n\n  // Progress\n  --progress-bg-light: rgba(0, 0, 0, 0.08);\n  --progress-inner-bg-light: var(--primary-color);\n\n  --progress-bg-dark: rgba(221, 221, 221, 0.08);\n  --progress-inner-bg-dark: var(--primary-color);\n\n  // Dropdown\n  --dropdown-bg-light: rgba(255, 255, 255, 0.8);\n  --dropdown-bg-dark: rgba(62, 62, 62, 0.8);\n\n  // Modal\n  --modal-bg-light: #f6f6f6;\n  --modal-mask-bg-light: rgba(255, 255, 255, 0.4);\n  --modal-bg-dark: #343434;\n  --modal-mask-bg-dark: rgba(0, 0, 0, 0.4);\n\n  // Switch\n  --switch-on-bg-light: var(--primary-color);\n  --switch-on-hover-bg-light: var(--secondary-color);\n  --switch-on-dot-bg-light: #fff;\n  --switch-on-bg-dark: var(--primary-color);\n  --switch-on-hover-bg-dark: var(--secondary-color);\n  --switch-on-dot-bg-dark: #fff;\n\n  --switch-off-bg-light: rgba(0, 0, 0, 0.06);\n  --switch-off-hover-bg-light: rgba(0, 0, 0, 0.1);\n  --switch-off-dot-bg-light: #fff;\n  --switch-off-bg-dark: rgba(255, 255, 255, 0.1);\n  --switch-off-hover-bg-dark: rgba(255, 255, 255, 0.06);\n  --switch-off-dot-bg-dark: #fff;\n\n  // Input\n  --input-color-light: #000;\n  --input-bg-light: rgba(255, 255, 255, 1);\n  --input-color-dark: #fff;\n  --input-bg-dark: rgba(255, 255, 255, 0.06);\n\n  // ColorPicker\n  --color-picker-bg-light: #fff;\n  --color-picker-bg-dark: rgba(255, 255, 255, 0.06);\n\n  // Divider\n  --divider-color-light: #c6c6c6;\n  --divider-color-dark: #4d4d4d;\n\n  // Select\n  --select-bg-light: rgba(255, 255, 255, 1);\n  --select-bg-dark: rgba(255, 255, 255, 0.06);\n\n  // Toast\n  --toast-bg-light: #fff;\n  --toast-bg-dark: #343434;\n\n  // Menu\n  --menu-bg-light: rgba(255, 255, 255, 0.8);\n  --menu-bg-dark: rgba(62, 62, 62, 0.8);\n\n  // Table\n  --table-tr-odd-bg-light: rgb(247, 247, 247);\n  --table-tr-even-bg-light: rgb(238, 238, 238);\n  --table-tr-odd-hover-bg-light: rgb(202, 202, 202);\n  --table-tr-even-hover-bg-light: rgb(202, 202, 202);\n  --table-tr-odd-bg-dark: #2e2e2e;\n  --table-tr-even-bg-dark: rgb(37, 37, 37);\n  --table-tr-odd-hover-bg-dark: rgb(61, 61, 61);\n  --table-tr-even-hover-bg-dark: rgb(61, 61, 61);\n\n  // Delay color\n  --level-0-color: #808080;\n  --level-1-color: #29b280;\n  --level-2-color: #b68b1f;\n  --level-3-color: #ea6060;\n  --level-4-color: #f00e0e;\n}\n"
  },
  {
    "path": "frontend/src/bridge/app.ts",
    "content": "import * as App from '@wails/go/bridge/App'\n\nexport const RestartApp = App.RestartApp\n\nexport const ExitApp = App.ExitApp\n\nexport const ShowMainWindow = App.ShowMainWindow\n\nexport const UpdateTray = App.UpdateTray\n\nexport const UpdateTrayMenus = App.UpdateTrayMenus\n\nexport const UpdateTrayAndMenus = App.UpdateTrayAndMenus\n\nexport const GetEnv = App.GetEnv\n\nexport const IsStartup = App.IsStartup\n\nexport const GetInterfaces = async () => {\n  const { flag, data } = await App.GetInterfaces()\n  if (!flag) {\n    throw data\n  }\n  return data.split('|')\n}\n"
  },
  {
    "path": "frontend/src/bridge/exec.ts",
    "content": "import * as App from '@wails/go/bridge/App'\nimport { EventsOn, EventsOff } from '@wails/runtime/runtime'\n\nimport { sampleID } from '@/utils'\n\ninterface ExecOptions {\n  PidFile?: string\n  Convert?: boolean\n  Env?: Record<string, any>\n  StopOutputKeyword?: string\n  WorkingDirectory?: string\n  convert?: boolean\n  env?: Record<string, any>\n  stopOutputKeyword?: string\n}\n\nconst mergeExecOptions = (options: ExecOptions) => {\n  const mergedExecOpts = {\n    PidFile: options.PidFile ?? '',\n    Convert: options.Convert ?? options.convert ?? false,\n    Env: options.Env ?? options.env ?? {},\n    StopOutputKeyword: options.StopOutputKeyword ?? options.stopOutputKeyword ?? '',\n    WorkingDirectory: options.WorkingDirectory ?? '',\n  }\n  return mergedExecOpts\n}\n\nexport const Exec = async (path: string, args: string[], options: ExecOptions = {}) => {\n  const { flag, data } = await App.Exec(path, args, mergeExecOptions(options))\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const ExecBackground = async (\n  path: string,\n  args: string[] = [],\n  onOut?: (out: string) => void,\n  onEnd?: () => void,\n  options: ExecOptions = {},\n) => {\n  const outEvent = (onOut && sampleID()) || ''\n  const endEvent = (onEnd && sampleID()) || (outEvent && sampleID()) || ''\n\n  const { flag, data } = await App.ExecBackground(\n    path,\n    args,\n    outEvent,\n    endEvent,\n    mergeExecOptions(options),\n  )\n  if (!flag) {\n    throw data\n  }\n\n  if (outEvent) {\n    EventsOn(outEvent, onOut!)\n  }\n\n  if (endEvent) {\n    EventsOn(endEvent, () => {\n      outEvent && EventsOff(outEvent)\n      EventsOff(endEvent)\n      onEnd?.()\n    })\n  }\n\n  return Number(data)\n}\n\nexport const ProcessInfo = async (pid: number) => {\n  const { flag, data } = await App.ProcessInfo(pid)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const ProcessMemory = async (pid: number) => {\n  const { flag, data } = await App.ProcessMemory(pid)\n  if (!flag) {\n    throw data\n  }\n  return Number(data)\n}\n\nexport const KillProcess = async (pid: number, timeout = 10) => {\n  const { flag, data } = await App.KillProcess(pid, timeout)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n"
  },
  {
    "path": "frontend/src/bridge/index.ts",
    "content": "export * from '@wails/runtime/runtime'\nexport * from './io'\nexport * from './net'\nexport * from './exec'\nexport * from './app'\nexport * from './server'\nexport * from './mmdb'\nexport * from './notification'\n"
  },
  {
    "path": "frontend/src/bridge/io.ts",
    "content": "import * as App from '@wails/go/bridge/App'\n\ninterface IOOptions {\n  Mode?: 'Binary' | 'Text'\n  Range?: string\n}\n\nexport const WriteFile = async (path: string, content: string, options: IOOptions = {}) => {\n  const { flag, data } = await App.WriteFile(path, content, { Mode: 'Text', Range: '', ...options })\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const ReadFile = async (path: string, options: IOOptions = {}) => {\n  const { flag, data } = await App.ReadFile(path, { Mode: 'Text', Range: '', ...options })\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const MoveFile = async (source: string, target: string) => {\n  const { flag, data } = await App.MoveFile(source, target)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const RemoveFile = async (path: string) => {\n  const { flag, data } = await App.RemoveFile(path)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const CopyFile = async (source: string, target: string) => {\n  const { flag, data } = await App.CopyFile(source, target)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const FileExists = async (path: string) => {\n  const { flag, data } = await App.FileExists(path)\n  if (!flag) {\n    throw data\n  }\n  return data === 'true'\n}\n\nexport const AbsolutePath = async (path: string) => {\n  const { flag, data } = await App.AbsolutePath(path)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const MakeDir = async (path: string) => {\n  const { flag, data } = await App.MakeDir(path)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const ReadDir = async (path: string) => {\n  const { flag, data } = await App.ReadDir(path)\n  if (!flag) {\n    throw data\n  }\n  return data\n    .split('|')\n    .filter((v) => v)\n    .map((v) => {\n      const [name, size, isDir] = v.split(',') as [string, string, string]\n      return { name, size: Number(size), isDir: isDir === 'true' }\n    })\n}\n\nexport const OpenDir = async (path: string) => {\n  const { flag, data } = await App.OpenDir(path)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const OpenURI = async (uri: string) => {\n  const { flag, data } = await App.OpenURI(uri)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const UnzipZIPFile = async (path: string, output: string) => {\n  const { flag, data } = await App.UnzipZIPFile(path, output)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const UnzipGZFile = async (path: string, output: string) => {\n  const { flag, data } = await App.UnzipGZFile(path, output)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const UnzipTarGZFile = async (path: string, output: string) => {\n  const { flag, data } = await App.UnzipTarGZFile(path, output)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n"
  },
  {
    "path": "frontend/src/bridge/mmdb.ts",
    "content": "import * as App from '@wails/go/bridge/App'\n\ntype QueryType =\n  | 'ASN'\n  | 'AnonymousIP'\n  | 'City'\n  | 'ConnectionType'\n  | 'Country'\n  | 'Domain'\n  | 'Enterprise'\n\nexport const OpenMMDB = async (path: string, id: string) => {\n  const { flag, data } = await App.OpenMMDB(path, id)\n  if (!flag) {\n    throw data\n  }\n  return {\n    close: () => CloseMMDB(path, id),\n    query: (ip: string, type: QueryType) => QueryMMDB(path, ip, type),\n  }\n}\n\nexport const CloseMMDB = async (path: string, id: string) => {\n  const { flag, data } = await App.CloseMMDB(path, id)\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n\nexport const QueryMMDB = async (path: string, ip: string, type: QueryType = 'Country') => {\n  const { flag, data } = await App.QueryMMDB(path, ip, type)\n  if (!flag) {\n    throw data\n  }\n  return JSON.parse(data)\n}\n"
  },
  {
    "path": "frontend/src/bridge/net.ts",
    "content": "import * as App from '@wails/go/bridge/App'\nimport { EventsOn, EventsOff, EventsEmit } from '@wails/runtime/runtime'\n\nimport { RequestMethod } from '@/enums/app'\nimport { sampleID, getUserAgent } from '@/utils'\nimport { GetSystemOrKernelProxy } from '@/utils/helper'\n\ninterface Request {\n  method: RequestMethod\n  url: string\n  headers?: {\n    'Content-Type'?: 'application/json' | 'application/x-www-form-urlencoded' | 'text/plain'\n  } & Record<string, string>\n  body?: any\n  options?: {\n    Proxy?: string\n    Insecure?: boolean\n    Redirect?: boolean\n    Timeout?: number\n    CancelId?: string\n    FileField?: string\n  }\n}\n\ninterface Response<T = any> {\n  status: number\n  headers: Record<string, string | string[]>\n  body: T\n}\n\nconst mergeRequestOptions = async (options: Request['options']) => {\n  const mergedReqOpts: Required<Request['options']> = {\n    Proxy: await GetSystemOrKernelProxy(),\n    Insecure: false,\n    Redirect: true,\n    Timeout: 15, // 15 seconds\n    CancelId: '',\n    FileField: 'file',\n    ...options,\n  }\n  return mergedReqOpts\n}\n\nconst transformResponseHeaders = (headers: Record<string, string[]>): Response['headers'] => {\n  return Object.fromEntries(\n    Object.entries(headers).map(([key, value]) => [key, value.length > 1 ? value : value[0]!]),\n  )\n}\n\nconst transformResponseBody = <T>(body: Response['body'], headers: Response['headers']) => {\n  if (headers['Content-Type']?.includes('application/json')) {\n    try {\n      body = JSON.parse(body)\n    } catch {\n      console.warn('Failed to parse response body as JSON:', body)\n    }\n  }\n  return body as T\n}\n\nconst transformRequest = async (\n  headers: Request['headers'],\n  body: Request['body'],\n  options: Request['options'],\n) => {\n  const transformedHeaders = { 'User-Agent': getUserAgent(), ...headers }\n\n  if (transformedHeaders['Content-Type']?.includes('application/json')) {\n    body && (body = JSON.stringify(body))\n  } else if (transformedHeaders['Content-Type']?.includes('application/x-www-form-urlencoded')) {\n    body && (body = new URLSearchParams(body).toString())\n  }\n\n  const transformedReqOpts = await mergeRequestOptions(options)\n  return [transformedHeaders, body, transformedReqOpts] as const\n}\n\nconst transformResponse = <T = any>(\n  status: Response['status'],\n  headers: Record<string, string[]>,\n  body: Response['body'],\n) => {\n  const transformedHeaders = transformResponseHeaders(headers)\n  const transformedBody = transformResponseBody<T>(body, transformedHeaders)\n\n  return { status, headers: transformedHeaders, body: transformedBody }\n}\n\ninterface RequestWithProgressOptions {\n  Method?: Request['method']\n}\n\nconst requestWithProgress = (fnName: 'Download' | 'Upload') => {\n  return async (\n    url: Request['url'],\n    path: string,\n    headers: Request['headers'] = {},\n    progress?: (progress: number, total: number) => void,\n    options: Request['options'] & RequestWithProgressOptions = {},\n  ) => {\n    const [_headers, , _options] = await transformRequest(headers, null, {\n      Timeout: 20 * 60, // 20 minutes\n      ...options,\n    })\n\n    const method =\n      options.Method ?? { Download: RequestMethod.Get, Upload: RequestMethod.Post }[fnName]\n\n    const progressEvent = (progress && sampleID()) || ''\n\n    if (progressEvent) {\n      EventsOn(progressEvent, progress!)\n    }\n\n    const {\n      flag,\n      status,\n      headers: respHeaders,\n      body: respBody,\n    } = await App[fnName](method, url, path, _headers, progressEvent, _options)\n\n    if (progressEvent) {\n      EventsOff(progressEvent)\n    }\n\n    if (!flag) throw respBody\n\n    return transformResponse(status, respHeaders, respBody)\n  }\n}\n\nconst requestWithBody = (method: RequestMethod.Put | RequestMethod.Post | RequestMethod.Patch) => {\n  return async <T = any>(\n    url: string,\n    headers: Request['headers'] = {},\n    body = {},\n    options = {},\n  ) => {\n    const [_headers, _body, _options] = await transformRequest(headers, body, options)\n\n    const {\n      flag,\n      status,\n      headers: respHeaders,\n      body: respBody,\n    } = await App.Requests(method, url, _headers, _body, _options)\n\n    if (!flag) throw respBody\n\n    return transformResponse<T>(status, respHeaders, respBody)\n  }\n}\n\nconst requestWithoutBody = (\n  methd: RequestMethod.Get | RequestMethod.Head | RequestMethod.Delete,\n) => {\n  return async <T = any>(\n    url: string,\n    headers: Request['headers'] = {},\n    options: Request['options'] = {},\n  ) => {\n    const [_headers, , _options] = await transformRequest(headers, null, options)\n\n    const {\n      flag,\n      status,\n      headers: respHeaders,\n      body,\n    } = await App.Requests(methd, url, _headers, '', _options)\n\n    if (!flag) throw body\n\n    return transformResponse<T>(status, respHeaders, body)\n  }\n}\n\ninterface RequestWithAutoTransform extends Request {\n  autoTransformBody?: boolean\n}\n\nexport const Requests = async <T = any>(options: RequestWithAutoTransform) => {\n  const { method = 'GET', url, headers = {}, body = '', options: reqOpts = {} } = options\n\n  const [reqHeaders, reqBody, finalReqOpts] = await transformRequest(headers, body, reqOpts)\n\n  const {\n    flag,\n    status,\n    headers: respHeaders,\n    body: respBody,\n  } = await App.Requests(method.toUpperCase(), url, reqHeaders, reqBody, finalReqOpts)\n\n  if (!flag) throw respBody\n\n  const transformedHeaders = transformResponseHeaders(respHeaders)\n  const transformBody = options.autoTransformBody ?? true\n\n  return {\n    status,\n    headers: transformedHeaders,\n    body: transformBody ? transformResponseBody<T>(respBody, transformedHeaders) : (respBody as T),\n  }\n}\n\nexport const Upload = requestWithProgress('Upload')\nexport const Download = requestWithProgress('Download')\n\nexport const HttpGet = requestWithoutBody(RequestMethod.Get)\nexport const HttpHead = requestWithoutBody(RequestMethod.Head)\nexport const HttpDelete = requestWithoutBody(RequestMethod.Delete)\n\nexport const HttpPut = requestWithBody(RequestMethod.Put)\nexport const HttpPost = requestWithBody(RequestMethod.Post)\nexport const HttpPatch = requestWithBody(RequestMethod.Patch)\n\nexport const HttpCancel = (cancelId: string) => EventsEmit(cancelId)\n"
  },
  {
    "path": "frontend/src/bridge/notification.ts",
    "content": "import * as App from '@wails/go/bridge/App'\n\nimport { APP_TITLE } from '@/utils'\n\ninterface NotifyOptions {\n  AppName?: string\n  Beep?: boolean\n}\n\nexport const Notify = async (\n  title: string,\n  message: string,\n  icon = '',\n  options: NotifyOptions = {},\n) => {\n  const _options: Required<NotifyOptions> = { AppName: APP_TITLE, Beep: true, ...options }\n  const icons: Record<string, string> = {\n    success: 'data/.cache/imgs/notify_success.png',\n    error: 'data/.cache/imgs/notify_error.png',\n  }\n  const { flag, data } = await App.Notify(\n    title,\n    message,\n    icons[icon] || 'data/.cache/imgs/tray_normal_dark.png',\n    _options,\n  )\n  if (!flag) {\n    throw data\n  }\n  return data\n}\n"
  },
  {
    "path": "frontend/src/bridge/server.ts",
    "content": "import * as App from '@wails/go/bridge/App'\nimport { EventsOn, EventsEmit, EventsOff } from '@wails/runtime/runtime'\n\ninterface Request {\n  id: string\n  method: string\n  url: string\n  headers: Record<string, string>\n  body: string\n}\n\ninterface Response {\n  status: number\n  headers: Record<string, string>\n  body: string\n  options: { mode: 'Binary' | 'Text' }\n}\n\ninterface ServerOptions {\n  Cert?: string\n  Key?: string\n  StaticPath?: string\n  StaticRoute?: string\n  StaticHeaders?: Recordable\n  UploadPath?: string\n  UploadRoute?: string\n  UploadHeaders?: Recordable\n  MaxUploadSize?: number\n}\n\ntype HttpServerHandler = (\n  req: Request,\n  res: {\n    end: (\n      status: Response['status'],\n      headers: Response['headers'],\n      body: Response['body'],\n      options: Response['options'],\n    ) => void\n  },\n) => Promise<void>\n\nexport const StartServer = async (\n  address: string,\n  id: string,\n  handler: HttpServerHandler,\n  options: ServerOptions = {},\n) => {\n  const _options: Required<ServerOptions> = {\n    Cert: '',\n    Key: '',\n    StaticPath: '', // default: /static\n    StaticRoute: '/static/',\n    StaticHeaders: {},\n    UploadPath: '', // default: /upload\n    UploadRoute: '/upload',\n    UploadHeaders: {},\n    MaxUploadSize: 50 * 1024 * 1024, // 50MB\n    ...options,\n  }\n  const { flag, data } = await App.StartServer(address, id, _options)\n  if (!flag) {\n    throw data\n  }\n\n  EventsOn(id, async (...args) => {\n    const [id, method, url, headers, body] = args\n    try {\n      await handler(\n        {\n          id,\n          method,\n          url,\n          headers: Object.entries(headers).reduce((p, c: any) => ({ ...p, [c[0]]: c[1][0] }), {}),\n          body,\n        },\n        {\n          end: (status, headers, body, options = { mode: 'Text' }) => {\n            EventsEmit(id, status, JSON.stringify(headers), body, JSON.stringify(options))\n          },\n        },\n      )\n    } catch (err: any) {\n      console.log('Server handler err:', err, id)\n      EventsEmit(\n        id,\n        500,\n        JSON.stringify({ 'Content-Type': 'text/plain; charset=utf-8' }),\n        err.message || err,\n        JSON.stringify({ Mode: 'Text' }),\n      )\n    }\n  })\n  return { close: () => StopServer(id) }\n}\n\nexport const StopServer = async (serverID: string) => {\n  const { flag, data } = await App.StopServer(serverID)\n  if (!flag) {\n    throw data\n  }\n  EventsOff(serverID)\n  return data\n}\n\nexport const ListServer = async () => {\n  const { flag, data } = await App.ListServer()\n  if (!flag) {\n    throw data\n  }\n  return data.split('|').filter((id) => id.length)\n}\n"
  },
  {
    "path": "frontend/src/bridge/wailsjs/go/bridge/App.d.ts",
    "content": "// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL\n// This file is automatically generated. DO NOT EDIT\nimport {bridge} from '../models';\n\nexport function AbsolutePath(arg1:string):Promise<bridge.FlagResult>;\n\nexport function CloseMMDB(arg1:string,arg2:string):Promise<bridge.FlagResult>;\n\nexport function CopyFile(arg1:string,arg2:string):Promise<bridge.FlagResult>;\n\nexport function Download(arg1:string,arg2:string,arg3:string,arg4:Record<string, string>,arg5:string,arg6:bridge.RequestOptions):Promise<bridge.HTTPResult>;\n\nexport function Exec(arg1:string,arg2:Array<string>,arg3:bridge.ExecOptions):Promise<bridge.FlagResult>;\n\nexport function ExecBackground(arg1:string,arg2:Array<string>,arg3:string,arg4:string,arg5:bridge.ExecOptions):Promise<bridge.FlagResult>;\n\nexport function ExitApp():Promise<void>;\n\nexport function FileExists(arg1:string):Promise<bridge.FlagResult>;\n\nexport function GetEnv():Promise<bridge.EnvResult>;\n\nexport function GetInterfaces():Promise<bridge.FlagResult>;\n\nexport function IsStartup():Promise<boolean>;\n\nexport function KillProcess(arg1:number,arg2:number):Promise<bridge.FlagResult>;\n\nexport function ListServer():Promise<bridge.FlagResult>;\n\nexport function MakeDir(arg1:string):Promise<bridge.FlagResult>;\n\nexport function MoveFile(arg1:string,arg2:string):Promise<bridge.FlagResult>;\n\nexport function Notify(arg1:string,arg2:string,arg3:string,arg4:bridge.NotifyOptions):Promise<bridge.FlagResult>;\n\nexport function OpenDir(arg1:string):Promise<bridge.FlagResult>;\n\nexport function OpenMMDB(arg1:string,arg2:string):Promise<bridge.FlagResult>;\n\nexport function OpenURI(arg1:string):Promise<bridge.FlagResult>;\n\nexport function ProcessInfo(arg1:number):Promise<bridge.FlagResult>;\n\nexport function ProcessMemory(arg1:number):Promise<bridge.FlagResult>;\n\nexport function QueryMMDB(arg1:string,arg2:string,arg3:string):Promise<bridge.FlagResult>;\n\nexport function ReadDir(arg1:string):Promise<bridge.FlagResult>;\n\nexport function ReadFile(arg1:string,arg2:bridge.IOOptions):Promise<bridge.FlagResult>;\n\nexport function RemoveFile(arg1:string):Promise<bridge.FlagResult>;\n\nexport function Requests(arg1:string,arg2:string,arg3:Record<string, string>,arg4:string,arg5:bridge.RequestOptions):Promise<bridge.HTTPResult>;\n\nexport function RestartApp():Promise<bridge.FlagResult>;\n\nexport function ShowMainWindow():Promise<void>;\n\nexport function StartServer(arg1:string,arg2:string,arg3:bridge.ServerOptions):Promise<bridge.FlagResult>;\n\nexport function StopServer(arg1:string):Promise<bridge.FlagResult>;\n\nexport function UnzipGZFile(arg1:string,arg2:string):Promise<bridge.FlagResult>;\n\nexport function UnzipTarGZFile(arg1:string,arg2:string):Promise<bridge.FlagResult>;\n\nexport function UnzipZIPFile(arg1:string,arg2:string):Promise<bridge.FlagResult>;\n\nexport function UpdateTray(arg1:bridge.TrayContent):Promise<void>;\n\nexport function UpdateTrayAndMenus(arg1:bridge.TrayContent,arg2:Array<bridge.MenuItem>):Promise<void>;\n\nexport function UpdateTrayMenus(arg1:Array<bridge.MenuItem>):Promise<void>;\n\nexport function Upload(arg1:string,arg2:string,arg3:string,arg4:Record<string, string>,arg5:string,arg6:bridge.RequestOptions):Promise<bridge.HTTPResult>;\n\nexport function WriteFile(arg1:string,arg2:string,arg3:bridge.IOOptions):Promise<bridge.FlagResult>;\n"
  },
  {
    "path": "frontend/src/bridge/wailsjs/go/bridge/App.js",
    "content": "// @ts-check\n// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL\n// This file is automatically generated. DO NOT EDIT\n\nexport function AbsolutePath(arg1) {\n  return window['go']['bridge']['App']['AbsolutePath'](arg1);\n}\n\nexport function CloseMMDB(arg1, arg2) {\n  return window['go']['bridge']['App']['CloseMMDB'](arg1, arg2);\n}\n\nexport function CopyFile(arg1, arg2) {\n  return window['go']['bridge']['App']['CopyFile'](arg1, arg2);\n}\n\nexport function Download(arg1, arg2, arg3, arg4, arg5, arg6) {\n  return window['go']['bridge']['App']['Download'](arg1, arg2, arg3, arg4, arg5, arg6);\n}\n\nexport function Exec(arg1, arg2, arg3) {\n  return window['go']['bridge']['App']['Exec'](arg1, arg2, arg3);\n}\n\nexport function ExecBackground(arg1, arg2, arg3, arg4, arg5) {\n  return window['go']['bridge']['App']['ExecBackground'](arg1, arg2, arg3, arg4, arg5);\n}\n\nexport function ExitApp() {\n  return window['go']['bridge']['App']['ExitApp']();\n}\n\nexport function FileExists(arg1) {\n  return window['go']['bridge']['App']['FileExists'](arg1);\n}\n\nexport function GetEnv() {\n  return window['go']['bridge']['App']['GetEnv']();\n}\n\nexport function GetInterfaces() {\n  return window['go']['bridge']['App']['GetInterfaces']();\n}\n\nexport function IsStartup() {\n  return window['go']['bridge']['App']['IsStartup']();\n}\n\nexport function KillProcess(arg1, arg2) {\n  return window['go']['bridge']['App']['KillProcess'](arg1, arg2);\n}\n\nexport function ListServer() {\n  return window['go']['bridge']['App']['ListServer']();\n}\n\nexport function MakeDir(arg1) {\n  return window['go']['bridge']['App']['MakeDir'](arg1);\n}\n\nexport function MoveFile(arg1, arg2) {\n  return window['go']['bridge']['App']['MoveFile'](arg1, arg2);\n}\n\nexport function Notify(arg1, arg2, arg3, arg4) {\n  return window['go']['bridge']['App']['Notify'](arg1, arg2, arg3, arg4);\n}\n\nexport function OpenDir(arg1) {\n  return window['go']['bridge']['App']['OpenDir'](arg1);\n}\n\nexport function OpenMMDB(arg1, arg2) {\n  return window['go']['bridge']['App']['OpenMMDB'](arg1, arg2);\n}\n\nexport function OpenURI(arg1) {\n  return window['go']['bridge']['App']['OpenURI'](arg1);\n}\n\nexport function ProcessInfo(arg1) {\n  return window['go']['bridge']['App']['ProcessInfo'](arg1);\n}\n\nexport function ProcessMemory(arg1) {\n  return window['go']['bridge']['App']['ProcessMemory'](arg1);\n}\n\nexport function QueryMMDB(arg1, arg2, arg3) {\n  return window['go']['bridge']['App']['QueryMMDB'](arg1, arg2, arg3);\n}\n\nexport function ReadDir(arg1) {\n  return window['go']['bridge']['App']['ReadDir'](arg1);\n}\n\nexport function ReadFile(arg1, arg2) {\n  return window['go']['bridge']['App']['ReadFile'](arg1, arg2);\n}\n\nexport function RemoveFile(arg1) {\n  return window['go']['bridge']['App']['RemoveFile'](arg1);\n}\n\nexport function Requests(arg1, arg2, arg3, arg4, arg5) {\n  return window['go']['bridge']['App']['Requests'](arg1, arg2, arg3, arg4, arg5);\n}\n\nexport function RestartApp() {\n  return window['go']['bridge']['App']['RestartApp']();\n}\n\nexport function ShowMainWindow() {\n  return window['go']['bridge']['App']['ShowMainWindow']();\n}\n\nexport function StartServer(arg1, arg2, arg3) {\n  return window['go']['bridge']['App']['StartServer'](arg1, arg2, arg3);\n}\n\nexport function StopServer(arg1) {\n  return window['go']['bridge']['App']['StopServer'](arg1);\n}\n\nexport function UnzipGZFile(arg1, arg2) {\n  return window['go']['bridge']['App']['UnzipGZFile'](arg1, arg2);\n}\n\nexport function UnzipTarGZFile(arg1, arg2) {\n  return window['go']['bridge']['App']['UnzipTarGZFile'](arg1, arg2);\n}\n\nexport function UnzipZIPFile(arg1, arg2) {\n  return window['go']['bridge']['App']['UnzipZIPFile'](arg1, arg2);\n}\n\nexport function UpdateTray(arg1) {\n  return window['go']['bridge']['App']['UpdateTray'](arg1);\n}\n\nexport function UpdateTrayAndMenus(arg1, arg2) {\n  return window['go']['bridge']['App']['UpdateTrayAndMenus'](arg1, arg2);\n}\n\nexport function UpdateTrayMenus(arg1) {\n  return window['go']['bridge']['App']['UpdateTrayMenus'](arg1);\n}\n\nexport function Upload(arg1, arg2, arg3, arg4, arg5, arg6) {\n  return window['go']['bridge']['App']['Upload'](arg1, arg2, arg3, arg4, arg5, arg6);\n}\n\nexport function WriteFile(arg1, arg2, arg3) {\n  return window['go']['bridge']['App']['WriteFile'](arg1, arg2, arg3);\n}\n"
  },
  {
    "path": "frontend/src/bridge/wailsjs/go/models.ts",
    "content": "export namespace bridge {\n\t\n\texport class EnvResult {\n\t    appName: string;\n\t    appVersion: string;\n\t    basePath: string;\n\t    os: string;\n\t    arch: string;\n\t    isPrivileged: boolean;\n\t\n\t    static createFrom(source: any = {}) {\n\t        return new EnvResult(source);\n\t    }\n\t\n\t    constructor(source: any = {}) {\n\t        if ('string' === typeof source) source = JSON.parse(source);\n\t        this.appName = source[\"appName\"];\n\t        this.appVersion = source[\"appVersion\"];\n\t        this.basePath = source[\"basePath\"];\n\t        this.os = source[\"os\"];\n\t        this.arch = source[\"arch\"];\n\t        this.isPrivileged = source[\"isPrivileged\"];\n\t    }\n\t}\n\texport class ExecOptions {\n\t    PidFile: string;\n\t    StopOutputKeyword: string;\n\t    WorkingDirectory: string;\n\t    Convert: boolean;\n\t    Env: Record<string, string>;\n\t\n\t    static createFrom(source: any = {}) {\n\t        return new ExecOptions(source);\n\t    }\n\t\n\t    constructor(source: any = {}) {\n\t        if ('string' === typeof source) source = JSON.parse(source);\n\t        this.PidFile = source[\"PidFile\"];\n\t        this.StopOutputKeyword = source[\"StopOutputKeyword\"];\n\t        this.WorkingDirectory = source[\"WorkingDirectory\"];\n\t        this.Convert = source[\"Convert\"];\n\t        this.Env = source[\"Env\"];\n\t    }\n\t}\n\texport class FlagResult {\n\t    flag: boolean;\n\t    data: string;\n\t\n\t    static createFrom(source: any = {}) {\n\t        return new FlagResult(source);\n\t    }\n\t\n\t    constructor(source: any = {}) {\n\t        if ('string' === typeof source) source = JSON.parse(source);\n\t        this.flag = source[\"flag\"];\n\t        this.data = source[\"data\"];\n\t    }\n\t}\n\texport class HTTPResult {\n\t    flag: boolean;\n\t    status: number;\n\t    headers: Record<string, Array<string>>;\n\t    body: string;\n\t\n\t    static createFrom(source: any = {}) {\n\t        return new HTTPResult(source);\n\t    }\n\t\n\t    constructor(source: any = {}) {\n\t        if ('string' === typeof source) source = JSON.parse(source);\n\t        this.flag = source[\"flag\"];\n\t        this.status = source[\"status\"];\n\t        this.headers = source[\"headers\"];\n\t        this.body = source[\"body\"];\n\t    }\n\t}\n\texport class IOOptions {\n\t    Mode: string;\n\t    Range: string;\n\t\n\t    static createFrom(source: any = {}) {\n\t        return new IOOptions(source);\n\t    }\n\t\n\t    constructor(source: any = {}) {\n\t        if ('string' === typeof source) source = JSON.parse(source);\n\t        this.Mode = source[\"Mode\"];\n\t        this.Range = source[\"Range\"];\n\t    }\n\t}\n\texport class MenuItem {\n\t    type: string;\n\t    text: string;\n\t    tooltip: string;\n\t    event: string;\n\t    children: MenuItem[];\n\t    hidden: boolean;\n\t    checked: boolean;\n\t\n\t    static createFrom(source: any = {}) {\n\t        return new MenuItem(source);\n\t    }\n\t\n\t    constructor(source: any = {}) {\n\t        if ('string' === typeof source) source = JSON.parse(source);\n\t        this.type = source[\"type\"];\n\t        this.text = source[\"text\"];\n\t        this.tooltip = source[\"tooltip\"];\n\t        this.event = source[\"event\"];\n\t        this.children = this.convertValues(source[\"children\"], MenuItem);\n\t        this.hidden = source[\"hidden\"];\n\t        this.checked = source[\"checked\"];\n\t    }\n\t\n\t\tconvertValues(a: any, classs: any, asMap: boolean = false): any {\n\t\t    if (!a) {\n\t\t        return a;\n\t\t    }\n\t\t    if (a.slice && a.map) {\n\t\t        return (a as any[]).map(elem => this.convertValues(elem, classs));\n\t\t    } else if (\"object\" === typeof a) {\n\t\t        if (asMap) {\n\t\t            for (const key of Object.keys(a)) {\n\t\t                a[key] = new classs(a[key]);\n\t\t            }\n\t\t            return a;\n\t\t        }\n\t\t        return new classs(a);\n\t\t    }\n\t\t    return a;\n\t\t}\n\t}\n\texport class NotifyOptions {\n\t    AppName: string;\n\t    Beep: boolean;\n\t\n\t    static createFrom(source: any = {}) {\n\t        return new NotifyOptions(source);\n\t    }\n\t\n\t    constructor(source: any = {}) {\n\t        if ('string' === typeof source) source = JSON.parse(source);\n\t        this.AppName = source[\"AppName\"];\n\t        this.Beep = source[\"Beep\"];\n\t    }\n\t}\n\texport class RequestOptions {\n\t    Proxy: string;\n\t    Insecure: boolean;\n\t    Redirect: boolean;\n\t    Timeout: number;\n\t    CancelId: string;\n\t    FileField: string;\n\t\n\t    static createFrom(source: any = {}) {\n\t        return new RequestOptions(source);\n\t    }\n\t\n\t    constructor(source: any = {}) {\n\t        if ('string' === typeof source) source = JSON.parse(source);\n\t        this.Proxy = source[\"Proxy\"];\n\t        this.Insecure = source[\"Insecure\"];\n\t        this.Redirect = source[\"Redirect\"];\n\t        this.Timeout = source[\"Timeout\"];\n\t        this.CancelId = source[\"CancelId\"];\n\t        this.FileField = source[\"FileField\"];\n\t    }\n\t}\n\texport class ServerOptions {\n\t    Cert: string;\n\t    Key: string;\n\t    StaticPath: string;\n\t    StaticRoute: string;\n\t    StaticHeaders: Record<string, string>;\n\t    UploadPath: string;\n\t    UploadRoute: string;\n\t    UploadHeaders: Record<string, string>;\n\t    MaxUploadSize: number;\n\t\n\t    static createFrom(source: any = {}) {\n\t        return new ServerOptions(source);\n\t    }\n\t\n\t    constructor(source: any = {}) {\n\t        if ('string' === typeof source) source = JSON.parse(source);\n\t        this.Cert = source[\"Cert\"];\n\t        this.Key = source[\"Key\"];\n\t        this.StaticPath = source[\"StaticPath\"];\n\t        this.StaticRoute = source[\"StaticRoute\"];\n\t        this.StaticHeaders = source[\"StaticHeaders\"];\n\t        this.UploadPath = source[\"UploadPath\"];\n\t        this.UploadRoute = source[\"UploadRoute\"];\n\t        this.UploadHeaders = source[\"UploadHeaders\"];\n\t        this.MaxUploadSize = source[\"MaxUploadSize\"];\n\t    }\n\t}\n\texport class TrayContent {\n\t    icon?: string;\n\t    title?: string;\n\t    tooltip?: string;\n\t\n\t    static createFrom(source: any = {}) {\n\t        return new TrayContent(source);\n\t    }\n\t\n\t    constructor(source: any = {}) {\n\t        if ('string' === typeof source) source = JSON.parse(source);\n\t        this.icon = source[\"icon\"];\n\t        this.title = source[\"title\"];\n\t        this.tooltip = source[\"tooltip\"];\n\t    }\n\t}\n\n}\n\n"
  },
  {
    "path": "frontend/src/bridge/wailsjs/runtime/package.json",
    "content": "{\n  \"name\": \"@wailsapp/runtime\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Wails Javascript runtime library\",\n  \"main\": \"runtime.js\",\n  \"types\": \"runtime.d.ts\",\n  \"scripts\": {\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wailsapp/wails.git\"\n  },\n  \"keywords\": [\n    \"Wails\",\n    \"Javascript\",\n    \"Go\"\n  ],\n  \"author\": \"Lea Anthony <lea.anthony@gmail.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/wailsapp/wails/issues\"\n  },\n  \"homepage\": \"https://github.com/wailsapp/wails#readme\"\n}\n"
  },
  {
    "path": "frontend/src/bridge/wailsjs/runtime/runtime.d.ts",
    "content": "/*\n _       __      _ __\n| |     / /___ _(_) /____\n| | /| / / __ `/ / / ___/\n| |/ |/ / /_/ / / (__  )\n|__/|__/\\__,_/_/_/____/\nThe electron alternative for Go\n(c) Lea Anthony 2019-present\n*/\n\nexport interface Position {\n    x: number;\n    y: number;\n}\n\nexport interface Size {\n    w: number;\n    h: number;\n}\n\nexport interface Screen {\n    isCurrent: boolean;\n    isPrimary: boolean;\n    width : number\n    height : number\n}\n\n// Environment information such as platform, buildtype, ...\nexport interface EnvironmentInfo {\n    buildType: string;\n    platform: string;\n    arch: string;\n}\n\n// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)\n// emits the given event. Optional data may be passed with the event.\n// This will trigger any event listeners.\nexport function EventsEmit(eventName: string, ...data: any): void;\n\n// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.\nexport function EventsOn(eventName: string, callback: (...data: any) => void): () => void;\n\n// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)\n// sets up a listener for the given event name, but will only trigger a given number times.\nexport function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;\n\n// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)\n// sets up a listener for the given event name, but will only trigger once.\nexport function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;\n\n// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)\n// unregisters the listener for the given event name.\nexport function EventsOff(eventName: string, ...additionalEventNames: string[]): void;\n\n// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)\n// unregisters all listeners.\nexport function EventsOffAll(): void;\n\n// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)\n// logs the given message as a raw message\nexport function LogPrint(message: string): void;\n\n// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)\n// logs the given message at the `trace` log level.\nexport function LogTrace(message: string): void;\n\n// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)\n// logs the given message at the `debug` log level.\nexport function LogDebug(message: string): void;\n\n// [LogError](https://wails.io/docs/reference/runtime/log#logerror)\n// logs the given message at the `error` log level.\nexport function LogError(message: string): void;\n\n// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)\n// logs the given message at the `fatal` log level.\n// The application will quit after calling this method.\nexport function LogFatal(message: string): void;\n\n// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)\n// logs the given message at the `info` log level.\nexport function LogInfo(message: string): void;\n\n// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)\n// logs the given message at the `warning` log level.\nexport function LogWarning(message: string): void;\n\n// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)\n// Forces a reload by the main application as well as connected browsers.\nexport function WindowReload(): void;\n\n// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)\n// Reloads the application frontend.\nexport function WindowReloadApp(): void;\n\n// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)\n// Sets the window AlwaysOnTop or not on top.\nexport function WindowSetAlwaysOnTop(b: boolean): void;\n\n// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)\n// *Windows only*\n// Sets window theme to system default (dark/light).\nexport function WindowSetSystemDefaultTheme(): void;\n\n// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)\n// *Windows only*\n// Sets window to light theme.\nexport function WindowSetLightTheme(): void;\n\n// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)\n// *Windows only*\n// Sets window to dark theme.\nexport function WindowSetDarkTheme(): void;\n\n// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)\n// Centers the window on the monitor the window is currently on.\nexport function WindowCenter(): void;\n\n// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)\n// Sets the text in the window title bar.\nexport function WindowSetTitle(title: string): void;\n\n// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)\n// Makes the window full screen.\nexport function WindowFullscreen(): void;\n\n// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)\n// Restores the previous window dimensions and position prior to full screen.\nexport function WindowUnfullscreen(): void;\n\n// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)\n// Returns the state of the window, i.e. whether the window is in full screen mode or not.\nexport function WindowIsFullscreen(): Promise<boolean>;\n\n// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)\n// Sets the width and height of the window.\nexport function WindowSetSize(width: number, height: number): void;\n\n// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)\n// Gets the width and height of the window.\nexport function WindowGetSize(): Promise<Size>;\n\n// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)\n// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.\n// Setting a size of 0,0 will disable this constraint.\nexport function WindowSetMaxSize(width: number, height: number): void;\n\n// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)\n// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.\n// Setting a size of 0,0 will disable this constraint.\nexport function WindowSetMinSize(width: number, height: number): void;\n\n// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)\n// Sets the window position relative to the monitor the window is currently on.\nexport function WindowSetPosition(x: number, y: number): void;\n\n// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)\n// Gets the window position relative to the monitor the window is currently on.\nexport function WindowGetPosition(): Promise<Position>;\n\n// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)\n// Hides the window.\nexport function WindowHide(): void;\n\n// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)\n// Shows the window, if it is currently hidden.\nexport function WindowShow(): void;\n\n// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)\n// Maximises the window to fill the screen.\nexport function WindowMaximise(): void;\n\n// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)\n// Toggles between Maximised and UnMaximised.\nexport function WindowToggleMaximise(): void;\n\n// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)\n// Restores the window to the dimensions and position prior to maximising.\nexport function WindowUnmaximise(): void;\n\n// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)\n// Returns the state of the window, i.e. whether the window is maximised or not.\nexport function WindowIsMaximised(): Promise<boolean>;\n\n// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)\n// Minimises the window.\nexport function WindowMinimise(): void;\n\n// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)\n// Restores the window to the dimensions and position prior to minimising.\nexport function WindowUnminimise(): void;\n\n// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)\n// Returns the state of the window, i.e. whether the window is minimised or not.\nexport function WindowIsMinimised(): Promise<boolean>;\n\n// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)\n// Returns the state of the window, i.e. whether the window is normal or not.\nexport function WindowIsNormal(): Promise<boolean>;\n\n// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)\n// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.\nexport function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;\n\n// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)\n// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.\nexport function ScreenGetAll(): Promise<Screen[]>;\n\n// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)\n// Opens the given URL in the system browser.\nexport function BrowserOpenURL(url: string): void;\n\n// [Environment](https://wails.io/docs/reference/runtime/intro#environment)\n// Returns information about the environment\nexport function Environment(): Promise<EnvironmentInfo>;\n\n// [Quit](https://wails.io/docs/reference/runtime/intro#quit)\n// Quits the application.\nexport function Quit(): void;\n\n// [Hide](https://wails.io/docs/reference/runtime/intro#hide)\n// Hides the application.\nexport function Hide(): void;\n\n// [Show](https://wails.io/docs/reference/runtime/intro#show)\n// Shows the application.\nexport function Show(): void;\n\n// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)\n// Returns the current text stored on clipboard\nexport function ClipboardGetText(): Promise<string>;\n\n// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)\n// Sets a text on the clipboard\nexport function ClipboardSetText(text: string): Promise<boolean>;\n\n// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)\n// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.\nexport function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void\n\n// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)\n// OnFileDropOff removes the drag and drop listeners and handlers.\nexport function OnFileDropOff() :void\n\n// Check if the file path resolver is available\nexport function CanResolveFilePaths(): boolean;\n\n// Resolves file paths for an array of files\nexport function ResolveFilePaths(files: File[]): void"
  },
  {
    "path": "frontend/src/bridge/wailsjs/runtime/runtime.js",
    "content": "/*\n _       __      _ __\n| |     / /___ _(_) /____\n| | /| / / __ `/ / / ___/\n| |/ |/ / /_/ / / (__  )\n|__/|__/\\__,_/_/_/____/\nThe electron alternative for Go\n(c) Lea Anthony 2019-present\n*/\n\nexport function LogPrint(message) {\n    window.runtime.LogPrint(message);\n}\n\nexport function LogTrace(message) {\n    window.runtime.LogTrace(message);\n}\n\nexport function LogDebug(message) {\n    window.runtime.LogDebug(message);\n}\n\nexport function LogInfo(message) {\n    window.runtime.LogInfo(message);\n}\n\nexport function LogWarning(message) {\n    window.runtime.LogWarning(message);\n}\n\nexport function LogError(message) {\n    window.runtime.LogError(message);\n}\n\nexport function LogFatal(message) {\n    window.runtime.LogFatal(message);\n}\n\nexport function EventsOnMultiple(eventName, callback, maxCallbacks) {\n    return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);\n}\n\nexport function EventsOn(eventName, callback) {\n    return EventsOnMultiple(eventName, callback, -1);\n}\n\nexport function EventsOff(eventName, ...additionalEventNames) {\n    return window.runtime.EventsOff(eventName, ...additionalEventNames);\n}\n\nexport function EventsOffAll() {\n  return window.runtime.EventsOffAll();\n}\n\nexport function EventsOnce(eventName, callback) {\n    return EventsOnMultiple(eventName, callback, 1);\n}\n\nexport function EventsEmit(eventName) {\n    let args = [eventName].slice.call(arguments);\n    return window.runtime.EventsEmit.apply(null, args);\n}\n\nexport function WindowReload() {\n    window.runtime.WindowReload();\n}\n\nexport function WindowReloadApp() {\n    window.runtime.WindowReloadApp();\n}\n\nexport function WindowSetAlwaysOnTop(b) {\n    window.runtime.WindowSetAlwaysOnTop(b);\n}\n\nexport function WindowSetSystemDefaultTheme() {\n    window.runtime.WindowSetSystemDefaultTheme();\n}\n\nexport function WindowSetLightTheme() {\n    window.runtime.WindowSetLightTheme();\n}\n\nexport function WindowSetDarkTheme() {\n    window.runtime.WindowSetDarkTheme();\n}\n\nexport function WindowCenter() {\n    window.runtime.WindowCenter();\n}\n\nexport function WindowSetTitle(title) {\n    window.runtime.WindowSetTitle(title);\n}\n\nexport function WindowFullscreen() {\n    window.runtime.WindowFullscreen();\n}\n\nexport function WindowUnfullscreen() {\n    window.runtime.WindowUnfullscreen();\n}\n\nexport function WindowIsFullscreen() {\n    return window.runtime.WindowIsFullscreen();\n}\n\nexport function WindowGetSize() {\n    return window.runtime.WindowGetSize();\n}\n\nexport function WindowSetSize(width, height) {\n    window.runtime.WindowSetSize(width, height);\n}\n\nexport function WindowSetMaxSize(width, height) {\n    window.runtime.WindowSetMaxSize(width, height);\n}\n\nexport function WindowSetMinSize(width, height) {\n    window.runtime.WindowSetMinSize(width, height);\n}\n\nexport function WindowSetPosition(x, y) {\n    window.runtime.WindowSetPosition(x, y);\n}\n\nexport function WindowGetPosition() {\n    return window.runtime.WindowGetPosition();\n}\n\nexport function WindowHide() {\n    window.runtime.WindowHide();\n}\n\nexport function WindowShow() {\n    window.runtime.WindowShow();\n}\n\nexport function WindowMaximise() {\n    window.runtime.WindowMaximise();\n}\n\nexport function WindowToggleMaximise() {\n    window.runtime.WindowToggleMaximise();\n}\n\nexport function WindowUnmaximise() {\n    window.runtime.WindowUnmaximise();\n}\n\nexport function WindowIsMaximised() {\n    return window.runtime.WindowIsMaximised();\n}\n\nexport function WindowMinimise() {\n    window.runtime.WindowMinimise();\n}\n\nexport function WindowUnminimise() {\n    window.runtime.WindowUnminimise();\n}\n\nexport function WindowSetBackgroundColour(R, G, B, A) {\n    window.runtime.WindowSetBackgroundColour(R, G, B, A);\n}\n\nexport function ScreenGetAll() {\n    return window.runtime.ScreenGetAll();\n}\n\nexport function WindowIsMinimised() {\n    return window.runtime.WindowIsMinimised();\n}\n\nexport function WindowIsNormal() {\n    return window.runtime.WindowIsNormal();\n}\n\nexport function BrowserOpenURL(url) {\n    window.runtime.BrowserOpenURL(url);\n}\n\nexport function Environment() {\n    return window.runtime.Environment();\n}\n\nexport function Quit() {\n    window.runtime.Quit();\n}\n\nexport function Hide() {\n    window.runtime.Hide();\n}\n\nexport function Show() {\n    window.runtime.Show();\n}\n\nexport function ClipboardGetText() {\n    return window.runtime.ClipboardGetText();\n}\n\nexport function ClipboardSetText(text) {\n    return window.runtime.ClipboardSetText(text);\n}\n\n/**\n * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.\n *\n * @export\n * @callback OnFileDropCallback\n * @param {number} x - x coordinate of the drop\n * @param {number} y - y coordinate of the drop\n * @param {string[]} paths - A list of file paths.\n */\n\n/**\n * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.\n *\n * @export\n * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.\n * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)\n */\nexport function OnFileDrop(callback, useDropTarget) {\n    return window.runtime.OnFileDrop(callback, useDropTarget);\n}\n\n/**\n * OnFileDropOff removes the drag and drop listeners and handlers.\n */\nexport function OnFileDropOff() {\n    return window.runtime.OnFileDropOff();\n}\n\nexport function CanResolveFilePaths() {\n    return window.runtime.CanResolveFilePaths();\n}\n\nexport function ResolveFilePaths(files) {\n    return window.runtime.ResolveFilePaths(files);\n}"
  },
  {
    "path": "frontend/src/components/Button/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { type IconName } from '@/components/Icon/icons'\n\ninterface Props {\n  type?: 'primary' | 'normal' | 'link' | 'text'\n  size?: 'default' | 'small' | 'large'\n  iconSize?: number\n  iconColor?: string\n  icon?: IconName\n  loading?: boolean\n  disabled?: boolean\n}\n\nwithDefaults(defineProps<Props>(), {\n  type: 'normal',\n  size: 'default',\n  iconSize: undefined,\n  iconColor: undefined,\n  icon: undefined,\n  loading: false,\n  disabled: false,\n})\n</script>\n\n<template>\n  <div\n    :class=\"[type, size, { 'pointer-events-none': disabled || loading }]\"\n    class=\"gui-button inline-flex items-center justify-center text-center align-middle rounded-6 text-14 text-nowrap cursor-pointer px-12 py-6 duration-200\"\n  >\n    <Icon\n      v-if=\"loading\"\n      :color=\"`var(--btn-${type}-color)`\"\n      :size=\"size === 'small' ? 14 : 16\"\n      icon=\"loading\"\n      class=\"rotation\"\n    />\n    <template v-else>\n      <Icon\n        v-if=\"disabled\"\n        :color=\"`var(--btn-${type}-color)`\"\n        icon=\"forbidden\"\n        class=\"pointer-events-none shrink-0 mr-4\"\n      />\n      <Icon\n        v-if=\"icon\"\n        :icon=\"icon\"\n        :size=\"iconSize\"\n        :color=\"iconColor || `var(--btn-${type}-color)`\"\n        :class=\"$slots.default ? 'mr-4' : ''\"\n      />\n    </template>\n    <slot></slot>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.normal {\n  color: var(--btn-normal-color);\n  background-color: var(--btn-normal-bg);\n  &:hover {\n    color: var(--btn-normal-hover-color);\n    border-color: var(--btn-normal-hover-border-color);\n  }\n  &:active {\n    color: var(--btn-normal-active-color);\n    border-color: var(--btn-normal-active-border-color);\n  }\n}\n\n.primary {\n  color: var(--btn-primary-color);\n  background-color: var(--btn-primary-bg);\n  border: none;\n  &:hover {\n    background-color: var(--btn-primary-hover-bg);\n  }\n  &:active {\n    background-color: var(--btn-primary-active-bg);\n  }\n}\n\n.link {\n  color: var(--btn-link-color);\n  background-color: var(--btn-link-bg);\n  border: none;\n  &:hover {\n    color: var(--btn-link-hover-color);\n  }\n  &:active {\n    color: var(--btn-link-active-color);\n  }\n}\n\n.text {\n  color: var(--btn-text-color);\n  background-color: var(--btn-text-bg);\n  border: none;\n  &:hover {\n    color: var(--btn-text-hover-color);\n    background-color: var(--btn-text-hover-bg);\n  }\n  &:active {\n    color: var(--btn-text-active-color);\n    background-color: var(--btn-text-active-bg);\n  }\n}\n\n.small {\n  padding: 4px 8px;\n  font-size: 12px;\n}\n.large {\n  padding: 8px 12px;\n  font-size: 16px;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Card/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, useSlots } from 'vue'\n\nimport vTips from '@/directives/tips'\n\ninterface Props {\n  title?: string\n  subtitle?: string\n  selected?: boolean\n  disabled?: boolean\n}\n\nconst props = defineProps<Props>()\n\nconst slots = useSlots()\n\nconst hasTitle = computed(() => {\n  return slots.extra || slots['title-prefix'] || slots['title-suffix'] || props.title\n})\n</script>\n\n<template>\n  <div class=\"gui-card rounded-8 relative flex flex-col\">\n    <div v-if=\"hasTitle\" class=\"card-header flex items-center break-all p-8\">\n      <slot name=\"title-prefix\"></slot>\n      <div v-if=\"title\" v-tips=\"title\" class=\"card-header_title line-clamp-1 text-16 font-bold\">\n        {{ title }}\n      </div>\n      <slot name=\"title-suffix\"></slot>\n      <div class=\"card-header_extra flex items-center ml-auto\">\n        <slot name=\"extra\"></slot>\n      </div>\n    </div>\n    <div v-if=\"subtitle\" class=\"card-header_subtitle mx-8\">{{ subtitle }}</div>\n    <div class=\"flex-1 px-8\" :class=\"hasTitle ? 'pb-8' : ''\">\n      <slot></slot>\n    </div>\n    <Icon\n      v-if=\"selected\"\n      :size=\"24\"\n      icon=\"selected\"\n      color=\"var(--primary-color)\"\n      class=\"absolute right-8 bottom-4\"\n    />\n    <Icon\n      v-if=\"disabled\"\n      :size=\"32\"\n      icon=\"disabled\"\n      color=\"var(--primary-color)\"\n      class=\"absolute right-8 bottom-4\"\n    />\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-card {\n  color: var(--card-color);\n  background-color: var(--card-bg);\n  transition:\n    box-shadow 0.2s,\n    background 0.2s;\n  &:hover {\n    box-shadow: 0 8px 8px rgba(0, 0, 0, 0.06);\n    background-color: var(--card-hover-bg);\n  }\n  &:active {\n    background-color: var(--card-active-bg);\n  }\n  &-header {\n    &_title {\n      color: var(--card-color);\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/CheckBox/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\ninterface Props {\n  modelValue?: string[]\n  options?: { label: string; value: string }[]\n  size?: 'default' | 'small'\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  modelValue: () => [],\n  options: () => [],\n  size: 'default',\n})\n\nconst emit = defineEmits(['change', 'update:modelValue'])\n\nconst model = ref<string[]>([...props.modelValue])\nconst { t } = useI18n()\n\nlet internalUpdate = false\n\nwatch(\n  () => props.modelValue,\n  (newVal) => {\n    if (!internalUpdate) {\n      model.value = [...newVal]\n    }\n    internalUpdate = false\n  },\n  {\n    deep: true,\n  },\n)\n\nconst isActive = (val: string) => model.value.includes(val)\n\nconst handleSelect = (val: string) => {\n  const idx = model.value.findIndex((v) => v === val)\n  if (idx !== -1) {\n    model.value.splice(idx, 1)\n  } else {\n    model.value.push(val)\n  }\n  internalUpdate = true\n  emit('update:modelValue', [...model.value])\n  emit('change', [...model.value])\n}\n</script>\n\n<template>\n  <div :class=\"[size]\" class=\"gui-checkbox inline-flex rounded-8 overflow-hidden text-12\">\n    <div\n      v-for=\"o in props.options\"\n      :key=\"o.value\"\n      :class=\"{ active: isActive(o.value) }\"\n      class=\"gui-checkbox-button cursor-pointer px-12 py-6 transition duration-200 line-clamp-1 break-all\"\n      @click=\"handleSelect(o.value)\"\n    >\n      {{ t(o.label) }}\n    </div>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-checkbox {\n  border: 1px solid var(--primary-color);\n  &-button {\n    color: var(--radio-normal-color);\n    background-color: var(--radio-normal-bg);\n    border-left: 1px solid var(--primary-color);\n    &:nth-child(1) {\n      border-left: none;\n    }\n    &:hover {\n      color: var(--radio-normal-hover-color);\n    }\n  }\n  .active {\n    color: var(--radio-primary-color);\n    background-color: var(--radio-primary-bg);\n    &:hover {\n      background-color: var(--radio-primary-hover-bg);\n    }\n    &:active {\n      background-color: var(--radio-primary-active-bg);\n    }\n  }\n}\n\n.small {\n  .gui-checkbox-button {\n    font-size: 10px;\n    padding: 4px 8px;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/CodeViewer/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { autocompletion } from '@codemirror/autocomplete'\nimport { indentWithTab } from '@codemirror/commands'\nimport { javascript } from '@codemirror/lang-javascript'\nimport { json, jsonParseLinter } from '@codemirror/lang-json'\nimport { yaml } from '@codemirror/lang-yaml'\nimport { linter } from '@codemirror/lint'\nimport { MergeView } from '@codemirror/merge'\nimport { Compartment } from '@codemirror/state'\nimport { oneDark } from '@codemirror/theme-one-dark'\nimport { keymap, placeholder as Placeholder } from '@codemirror/view'\nimport { EditorView, basicSetup } from 'codemirror'\nimport * as parserBabel from 'prettier/parser-babel'\nimport * as parserYaml from 'prettier/parser-yaml'\nimport estreePlugin from 'prettier/plugins/estree'\nimport * as prettier from 'prettier/standalone'\nimport { watch, onUnmounted, onMounted, useTemplateRef, inject } from 'vue'\n\nimport { Theme } from '@/enums/app'\nimport { useAppSettingsStore } from '@/stores'\nimport { debounce, message } from '@/utils'\nimport { getCompletions } from '@/utils/completion'\n\nimport { IS_IN_MODAL } from '@/components/Modal/index.vue'\n\ninterface Props {\n  modelValue?: string\n  editable?: boolean\n  lang?: 'json' | 'javascript' | 'yaml'\n  mode?: 'editor' | 'diff'\n  placeholder?: string\n  plugin?: Record<string, any>\n}\n\nconst emit = defineEmits(['change', 'update:modelValue'])\nconst props = withDefaults(defineProps<Props>(), {\n  modelValue: '',\n  lang: 'json',\n  mode: 'editor',\n  placeholder: '',\n  plugin: undefined,\n})\n\nconst { promise: editorReady, resolve: markEditorReady } = Promise.withResolvers()\nlet internalUpdate = true\n\nwatch(\n  () => props.modelValue,\n  async (val) => {\n    await editorReady\n    const view = editorView || mergeView?.b\n    if (view && val != view.state.doc.toString()) {\n      internalUpdate = false\n      view.dispatch({\n        changes: {\n          from: 0,\n          to: view.state.doc.length,\n          insert: val,\n        },\n      })\n    }\n  },\n)\n\nlet editorView: EditorView\nlet mergeView: MergeView\nconst themeCompartment = new Compartment()\nconst domRef = useTemplateRef('domRef')\nconst appSettings = useAppSettingsStore()\n\nconst onChange = debounce((content: string) => {\n  if (internalUpdate) {\n    emit('update:modelValue', content)\n    emit('change', content)\n  }\n  internalUpdate = true\n}, 300)\n\nconst formatDoc = async (view: EditorView) => {\n  const content = view.state.doc.toString()\n  const cursor = view.state.selection.ranges[0]?.from || 0\n  try {\n    const parser = { javascript: 'babel', yaml: 'yaml', json: 'json' }[props.lang]\n    const plugins = {\n      javascript: [parserBabel, estreePlugin],\n      yaml: [parserYaml],\n      json: [parserBabel, estreePlugin],\n    }[props.lang]\n    const { formatted, cursorOffset } = await prettier.formatWithCursor(content, {\n      cursorOffset: cursor,\n      parser,\n      plugins,\n      // https://github.com/GUI-for-Cores/Plugin-Hub/blob/main/.prettierrc.json\n      semi: false,\n      tabWidth: 2,\n      singleQuote: true,\n      printWidth: 160,\n      trailingComma: 'none',\n    })\n    if (content !== formatted) {\n      view.dispatch({\n        changes: { from: 0, to: content.length, insert: formatted },\n        selection: { anchor: cursorOffset, head: cursorOffset },\n      })\n    }\n  } catch (error: any) {\n    message.error(error.message || error)\n  }\n}\n\nwatch(\n  () => appSettings.themeMode,\n  (theme) => {\n    const views = editorView ? [editorView] : [mergeView.a, mergeView.b]\n    views.forEach((view) => {\n      view.dispatch({\n        effects: themeCompartment.reconfigure(\n          theme === Theme.Dark ? [EditorView.theme({}, { dark: true }), oneDark] : [],\n        ),\n      })\n    })\n  },\n)\n\nlet timer: number\nonMounted(() => (timer = setTimeout(() => initEditor(), inject(IS_IN_MODAL, false) ? 100 : 0)))\nonUnmounted(() => {\n  clearTimeout(timer)\n  const view = editorView || mergeView\n  view?.destroy()\n})\n\nconst initEditor = () => {\n  domRef.value!.innerHTML = ''\n\n  const extensions = [\n    basicSetup,\n    // keymap\n    keymap.of([\n      indentWithTab,\n      {\n        key: 'Shift-Alt-f',\n        run: function (v: EditorView) {\n          formatDoc(v)\n          return true\n        },\n      },\n    ]),\n    // code wrap\n    EditorView.lineWrapping,\n    // placeholder\n    Placeholder(props.placeholder),\n    // theme\n    themeCompartment.of(\n      appSettings.themeMode === Theme.Dark ? [EditorView.theme({}, { dark: true }), oneDark] : [],\n    ),\n    ...(props.lang === 'javascript'\n      ? [autocompletion({ override: getCompletions(props.plugin) })]\n      : []),\n    // lint\n    ...(props.lang === 'json' ? [linter(jsonParseLinter())] : []),\n    // lang\n    ...(['javascript', 'json', 'yaml'].includes(props.lang)\n      ? [{ javascript, json, yaml }[props.lang]()]\n      : []),\n    EditorView.updateListener.of((update) => {\n      update.docChanged && onChange(update.state.doc.toString())\n    }),\n  ]\n\n  if (props.mode === 'editor') {\n    editorView = new EditorView({\n      doc: props.modelValue,\n      parent: domRef.value!,\n      extensions: [...extensions, EditorView.editable.of(props.editable)],\n    })\n  } else {\n    mergeView = new MergeView({\n      parent: domRef.value!,\n      a: {\n        doc: props.modelValue,\n        extensions: [...extensions, EditorView.editable.of(false)],\n      },\n      b: {\n        doc: props.modelValue,\n        extensions: [...extensions, EditorView.editable.of(props.editable)],\n      },\n    })\n  }\n\n  markEditorReady(null)\n}\n</script>\n\n<template>\n  <div ref=\"domRef\" @keydown.esc.stop @keydown.esc.prevent>\n    <div class=\"flex justify-center\">\n      <Button loading type=\"link\" />\n    </div>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n:deep(.cm-editor) {\n  height: 100%;\n}\n:deep(.cm-scroller) {\n  font-family: monaco, Consolas, Menlo, Courier, monospace;\n  font-size: 14px;\n}\n:deep(.cm-focused) {\n  outline: none;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/ColorPicker/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useTemplateRef } from 'vue'\n\ninterface Props {\n  disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  disabled: false,\n})\n\nconst model = defineModel<string>({ default: '#000000' })\n\nconst emit = defineEmits(['change'])\n\nconst inputRef = useTemplateRef('inputRef')\n\nconst onChange = (v: Event) => {\n  const val = (v.target as HTMLInputElement).value\n  emit('change', val)\n}\n\nconst pick = () => {\n  !props.disabled && inputRef.value?.click()\n}\n</script>\n\n<template>\n  <div\n    :class=\"{\n      'pl-8': $slots.prefix,\n      'pr-8': $slots.suffix,\n      'cursor-not-allowed': disabled,\n      'cursor-pointer': !disabled,\n    }\"\n    class=\"gui-color-picker rounded-full inline-flex items-center overflow-hidden duration-200\"\n    @click=\"pick\"\n  >\n    <div v-if=\"$slots.prefix\" class=\"flex items-center line-clamp-1 break-all\">\n      <slot name=\"prefix\" v-bind=\"{ pick }\"></slot>\n    </div>\n    <input\n      ref=\"inputRef\"\n      v-model=\"model\"\n      :class=\"{ 'pointer-events-none': disabled }\"\n      type=\"color\"\n      class=\"w-26 h-28 flex justify-center items-center border-0 bg-transparent cursor-pointer\"\n      @change=\"(e) => onChange(e)\"\n    />\n    <div v-if=\"$slots.suffix\" class=\"flex items-center line-clamp-1 break-all\">\n      <slot name=\"suffix\" v-bind=\"{ pick }\"></slot>\n    </div>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-color-picker {\n  color: var(--color);\n  border: 1px solid var(--primary-color);\n  background: var(--color-picker-bg);\n\n  &:hover {\n    color: var(--primary-color);\n  }\n}\n\ninput::-webkit-color-swatch-wrapper {\n  padding: 0;\n  width: 16px;\n  height: 16px;\n}\n\ninput::-webkit-color-swatch {\n  border-radius: 8px;\n  border: none;\n}\n\nbody[feature-no-rounded='true'] {\n  input::-webkit-color-swatch {\n    border-radius: 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Confirm/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { marked } from 'marked'\nimport { h, onMounted, ref, render, type VNode } from 'vue'\n\nimport useI18n from '@/lang'\nimport { APP_TITLE, APP_VERSION, sampleID } from '@/utils'\n\nimport CodeViewer from '@/components/CodeViewer/index.vue'\nimport Divider from '@/components/Divider/index.vue'\nimport Table from '@/components/Table/index.vue'\nimport Tag from '@/components/Tag/index.vue'\n\nimport type { Column } from '@/components/Table/index.vue'\n\nexport type ConfirmOptions = {\n  type: 'text' | 'markdown'\n  cancelText?: string\n  okText?: string\n}\n\ninterface Props {\n  title: string\n  message: string | Record<string, any>\n  options?: ConfirmOptions\n  cancel?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  cancel: true,\n  options: () => ({ type: 'text' }),\n})\n\nconst emits = defineEmits(['confirm', 'cancel', 'finish'])\n\nconst content = ref<string | Record<string, any>>('')\nconst domContainers: (() => void)[] = []\n\nconst { t } = useI18n.global\n\nmarked.setOptions({ async: true })\n\nmarked.use({\n  renderer: {\n    image({ href, title, text }) {\n      return `<img src=\"${href}\" alt=\"${title || text}\" style=\"max-width: 100%\">`\n    },\n    link({ href, title }) {\n      return `<span onclick=\"Plugins.BrowserOpenURL('${href}')\" style=\"color: var(--primary-color); cursor: pointer\">${title || href}</span>`\n    },\n    blockquote({ tokens }) {\n      const text = this.parser.parse(tokens)\n      return `<div style=\"border-left: 4px solid var(--primary-color); padding: 8px; margin: 8px 0; display: flex; flex-direction: column; border-radius: 4px; background: var(--card-bg)\">${text}</div>`\n    },\n    paragraph({ tokens }) {\n      const text = this.parser.parseInline(tokens)\n      return `<p style=\"margin: 0\">${text}</p>`\n    },\n    list({ ordered, items }) {\n      const children = items.reduce((str, { tokens }) => {\n        const text = this.parser.parse(tokens)\n        return str + `<li style=\"padding: 0\">${text}</li>`\n      }, '')\n      const tag = ordered ? 'ol' : 'ul'\n      return `<${tag} style=\"margin: 0; padding: 8px 16px\">${children}</${tag}>`\n    },\n    hr() {\n      const containerId = 'Divider_' + sampleID()\n      const comp = h(Divider, () => APP_TITLE + '/' + APP_VERSION)\n      mountCustomComp(containerId, comp)\n      return `<div id=\"${containerId}\"></div>`\n    },\n    heading({ text, depth }) {\n      return `<h${depth} style=\"color: var(--primary-color)\"># ${text}</h${depth}>`\n    },\n    codespan({ text }) {\n      const containerId = 'Tag_' + sampleID()\n      const comp = h(Tag, { color: 'cyan', size: 'small' }, () => text)\n      mountCustomComp(containerId, comp)\n      return `<span id=\"${containerId}\"></span>`\n    },\n    code({ text, lang }) {\n      const containerId = 'CodeViewer_' + sampleID()\n      const comp = h(CodeViewer, { editable: false, modelValue: text, lang: lang as any })\n      mountCustomComp(containerId, comp)\n      return `<div id=\"${containerId}\"></div>`\n    },\n    table({ header, rows }) {\n      const containerId = 'Table_' + sampleID()\n      const comp = h(Table, {\n        columns: header.map<Column>(({ text, align }) => ({\n          title: text,\n          key: text,\n          align: align || 'center',\n          customRender: ({ value }) => h('div', { innerHTML: value }),\n        })),\n        dataSource: rows.map((row) => {\n          const record: Record<string, any> = {}\n          header.forEach(({ text }, index) => {\n            record[text] = this.parser.parseInline(row[index]?.tokens || [])\n          })\n          return record\n        }),\n      })\n      mountCustomComp(containerId, comp)\n      return `<div id=\"${containerId}\"></div>`\n    },\n  },\n})\n\nconst mountCustomComp = (containerId: string, comp: VNode) => {\n  let count = 0\n  comp.appContext = window.appInstance._context\n  const tryToMount = () => {\n    if (count >= 3) return\n    count += 1\n    const div = document.getElementById(containerId)\n    if (!div) return setTimeout(tryToMount, count * 100)\n    render(comp, div)\n    domContainers.push(() => render(null, div))\n  }\n  setTimeout(tryToMount)\n}\n\nconst renderContent = async () => {\n  if (typeof props.message !== 'string') {\n    content.value = JSON.stringify(props.message, null, 2)\n    return\n  }\n  if (props.options.type === 'text') {\n    content.value = t(props.message)\n    return\n  }\n  content.value = await marked.parse(props.message)\n}\n\nonMounted(renderContent)\n\nconst handleConfirm = () => {\n  emits('confirm', true)\n  emits('finish')\n  domContainers.forEach((destroy) => destroy())\n}\n\nconst handleCancel = () => {\n  emits('cancel')\n  emits('finish')\n  domContainers.forEach((destroy) => destroy())\n}\n</script>\n\n<template>\n  <Transition name=\"slide-down\" appear>\n    <div class=\"gui-confirm flex flex-col p-8 rounded-8 shadow\">\n      <div class=\"font-bold break-all px-4 py-8\">{{ t(title) }}</div>\n      <div\n        v-if=\"options.type === 'markdown'\"\n        class=\"flex-1 overflow-y-auto text-12 leading-relaxed p-6 break-all whitespace-pre-wrap select-text\"\n        v-html=\"content\"\n      ></div>\n      <div\n        v-else\n        class=\"flex-1 overflow-y-auto text-12 leading-relaxed p-6 break-all whitespace-pre-wrap select-text\"\n      >\n        {{ content }}\n      </div>\n      <div class=\"form-action gap-4\">\n        <Button v-if=\"cancel\" size=\"small\" @click=\"handleCancel\">\n          {{ t(options.cancelText || 'common.cancel') }}\n        </Button>\n        <Button size=\"small\" type=\"primary\" @click=\"handleConfirm\">\n          {{ t(options.okText || 'common.confirm') }}\n        </Button>\n      </div>\n    </div>\n  </Transition>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-confirm {\n  min-width: 340px;\n  max-width: 60%;\n  background: var(--toast-bg);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/CustomAction/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, h, isVNode, ref, resolveComponent, watch } from 'vue'\n\nimport type { CustomAction, CustomActionFn, CustomActionApi } from '@/types/app'\n\ninterface Props {\n  actions: (CustomAction | CustomActionFn)[]\n}\n\nconst props = defineProps<Props>()\n\nconst resolvedActionMap = ref(new Map<string, CustomAction>())\nconst api: CustomActionApi = {\n  h: (type: any, ...args: any[]) => h(resolveComponent(type), ...args),\n  ref,\n}\n\nconst computedActions = computed(() => Array.from(resolvedActionMap.value.values()))\n\nconst resolveDynamicField = <T,>(field: T): T => (typeof field === 'function' ? field(api) : field)\n\nconst renderCustomActionSlot = (slot: CustomAction['componentSlots']) => {\n  const resolved = resolveDynamicField(slot ?? {})\n  return isVNode(resolved) ? resolved : h('div', resolved)\n}\n\nwatch(\n  () => props.actions,\n  (actions) => {\n    const newMap = new Map<string, CustomAction>()\n    for (const action of actions) {\n      const id = action.id!\n      if (resolvedActionMap.value.has(id)) {\n        newMap.set(id, resolvedActionMap.value.get(id)!)\n      } else {\n        newMap.set(id, typeof action === 'function' ? action(api) : action)\n      }\n    }\n    resolvedActionMap.value = newMap\n  },\n  { immediate: true, deep: true },\n)\n</script>\n<template>\n  <component\n    :is=\"action.component\"\n    v-for=\"action in computedActions\"\n    :key=\"action.id\"\n    v-memo=\"action.id\"\n    v-bind=\"resolveDynamicField(action.componentProps)\"\n  >\n    <template\n      v-for=\"[name, slot] in Object.entries(resolveDynamicField(action.componentSlots ?? {}))\"\n      :key=\"name\"\n      #[name]\n    >\n      <component :is=\"renderCustomActionSlot(slot)\" />\n    </template>\n  </component>\n</template>\n"
  },
  {
    "path": "frontend/src/components/Divider/index.vue",
    "content": "<template>\n  <div class=\"flex items-center w-full\">\n    <div class=\"flex-1\" style=\"border-top: 1px solid var(--divider-color)\"></div>\n    <div class=\"text-12 p-8\">\n      <slot></slot>\n    </div>\n    <div class=\"flex-1\" style=\"border-top: 1px solid var(--divider-color)\"></div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/components/Dropdown/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, ref, watch, nextTick, useTemplateRef } from 'vue'\n\nimport { debounce } from '@/utils'\n\ntype TriggerType = 'click' | 'hover'\n\ninterface Props {\n  trigger?: TriggerType[]\n  placement?: 'bottom' | 'top'\n  delay?: number\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  trigger: () => ['hover'],\n  placement: 'bottom',\n  delay: 0,\n})\n\nconst domRef = useTemplateRef('domRef')\nconst overlayRef = useTemplateRef('overlayRef')\nconst overlayStyle = ref({ top: 'auto', left: '0px', bottom: 'auto', maxHeight: 'none' })\nconst show = ref(false)\nconst transformOrigin = ref(props.placement === 'top' ? 'bottom' : 'top')\n\nconst updatePosition = () => {\n  if (!domRef.value || !overlayRef.value || !show.value) return\n\n  const triggerRect = domRef.value.getBoundingClientRect()\n  const overlayEl = overlayRef.value\n\n  overlayEl.style.minWidth = `${triggerRect.width}px`\n\n  const overlayHeight = overlayEl.offsetHeight\n  const overlayWidth = overlayEl.offsetWidth\n\n  const screenEdgeMargin = 8\n\n  const totalSpaceBelow = window.innerHeight - triggerRect.bottom\n  const totalSpaceAbove = triggerRect.top\n\n  let finalPlacement: 'top' | 'bottom'\n\n  const canPlaceBottom = totalSpaceBelow >= overlayHeight\n  const canPlaceTop = totalSpaceAbove >= overlayHeight\n\n  if (props.placement === 'bottom') {\n    if (canPlaceBottom) {\n      finalPlacement = 'bottom'\n    } else if (canPlaceTop) {\n      finalPlacement = 'top'\n    } else {\n      finalPlacement = totalSpaceBelow > totalSpaceAbove ? 'bottom' : 'top'\n    }\n  } else {\n    if (canPlaceTop) {\n      finalPlacement = 'top'\n    } else if (canPlaceBottom) {\n      finalPlacement = 'bottom'\n    } else {\n      finalPlacement = totalSpaceAbove > totalSpaceBelow ? 'top' : 'bottom'\n    }\n  }\n\n  transformOrigin.value = finalPlacement === 'top' ? 'bottom' : 'top'\n\n  if (finalPlacement === 'bottom') {\n    overlayStyle.value.top = `${triggerRect.bottom}px`\n    overlayStyle.value.bottom = 'auto'\n    const availableHeight = totalSpaceBelow - screenEdgeMargin\n    overlayStyle.value.maxHeight = `${Math.max(0, availableHeight)}px`\n  } else {\n    overlayStyle.value.bottom = `${window.innerHeight - triggerRect.top}px`\n    overlayStyle.value.top = 'auto'\n    const availableHeight = totalSpaceAbove - screenEdgeMargin\n    overlayStyle.value.maxHeight = `${Math.max(0, availableHeight)}px`\n  }\n\n  let left = triggerRect.left + triggerRect.width / 2 - overlayWidth / 2\n\n  if (left + overlayWidth > window.innerWidth - screenEdgeMargin) {\n    left = window.innerWidth - overlayWidth - screenEdgeMargin\n  }\n  if (left < screenEdgeMargin) {\n    left = screenEdgeMargin\n  }\n  overlayStyle.value.left = `${left}px`\n}\n\nwatch(show, async (isVisible) => {\n  if (isVisible) {\n    await nextTick()\n    updatePosition()\n    window.addEventListener('scroll', updatePosition, true)\n    window.addEventListener('resize', updatePosition)\n  } else {\n    window.removeEventListener('scroll', updatePosition, true)\n    window.removeEventListener('resize', updatePosition)\n  }\n})\n\nconst open = () => (show.value = true)\nconst close = () => (show.value = false)\nconst toggle = () => (show.value = !show.value)\nconst hasTrigger = (t: TriggerType) => props.trigger.includes(t)\n\nconst debounceOpen = debounce(open, props.delay)\n\nconst onMouseEnter = () => {\n  if (hasTrigger('hover')) {\n    debounceOpen()\n  }\n}\n\nconst onMouseLeave = () => {\n  if (hasTrigger('hover')) {\n    debounceOpen.cancel()\n    close()\n  }\n}\n\nconst onClick = () => {\n  if (hasTrigger('click')) {\n    show.value = !show.value\n  }\n}\n\nconst onDomClick = (e: MouseEvent) => {\n  if (!domRef.value?.contains(e.target as Node) && !overlayRef.value?.contains(e.target as Node)) {\n    close()\n  }\n}\n\nonMounted(() => {\n  if (hasTrigger('click')) {\n    document.addEventListener('click', onDomClick)\n  }\n})\n\nonUnmounted(() => {\n  if (hasTrigger('click')) {\n    document.removeEventListener('click', onDomClick)\n  }\n  window.removeEventListener('scroll', updatePosition, true)\n  window.removeEventListener('resize', updatePosition)\n})\n</script>\n\n<template>\n  <div\n    ref=\"domRef\"\n    class=\"gui-dropdown relative inline-flex flex-col items-center\"\n    @mouseenter=\"onMouseEnter\"\n    @mouseleave=\"onMouseLeave\"\n    @click=\"onClick\"\n  >\n    <slot v-bind=\"{ open, close, toggle }\"></slot>\n    <Transition name=\"overlay\">\n      <div\n        v-show=\"show\"\n        ref=\"overlayRef\"\n        :style=\"overlayStyle\"\n        class=\"gui-dropdown-overlay fixed z-99 rounded-8 backdrop-blur-sm shadow overflow-y-auto\"\n        @click.stop\n      >\n        <slot name=\"overlay\" v-bind=\"{ open, close, toggle }\"></slot>\n      </div>\n    </Transition>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.overlay-enter-active,\n.overlay-leave-active {\n  transition:\n    transform 0.2s ease-in-out,\n    opacity 0.2s ease-in-out;\n  transform-origin: v-bind(transformOrigin);\n}\n\n.overlay-enter-from,\n.overlay-leave-to {\n  opacity: 0;\n  transform: scaleY(0);\n}\n\n.gui-dropdown-overlay {\n  background: var(--dropdown-bg);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Empty/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { IconName } from '@/components/Icon/icons'\n\ninterface Props {\n  icon?: IconName\n  iconSize?: number\n  description?: string\n}\n\nwithDefaults(defineProps<Props>(), {\n  icon: 'empty',\n  iconSize: 64,\n  description: 'common.empty',\n})\n</script>\n\n<template>\n  <div class=\"gui-empty flex flex-col w-full h-full items-center justify-center\">\n    <Icon :icon=\"icon\" :size=\"iconSize\" />\n    <slot name=\"description\">\n      <div class=\"text-12 py-8\">{{ $t(description) }}</div>\n    </slot>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/components/Icon/icons.ts",
    "content": "export const icons = {\n  messageSuccess: `<svg viewBox=\"64 64 896 896\"><path d=\"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z\" fill=\"#52c41a\" /></svg>`,\n  messageError: `<svg viewBox=\"64 64 896 896\"><path d=\"M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z\" fill=\"#ff4d4f\" /></svg>`,\n  messageInfo: `<svg viewBox=\"64 64 896 896\"><path d=\"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z\" fill=\"#b8b8b8\" /></svg>`,\n  messageWarn: `<svg viewBox=\"64 64 896 896\"><path d=\"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z\" fill=\"#faad14\" /></svg>`,\n  clear: `<svg viewBox=\"64 64 896 896\"><path d=\"M433.664 250.88L773.12 590.336 599.466667 837.162667a42.666667 42.666667 0 0 1-65.066667 5.632l-61.333333-61.333334v-130.88h-130.858667L181.205333 489.6a42.666667 42.666667 0 0 1 5.632-65.066667l246.826667-173.632z m38.378667-26.986667l66.133333-46.528a42.666667 42.666667 0 0 1 54.72 4.714667l89.130667 89.152 93.781333-93.781333a21.333333 21.333333 0 0 1 30.165333 0l35.2 35.2a21.333333 21.333333 0 0 1 0 30.186666l-93.76 93.76 94.506667 94.506667a42.666667 42.666667 0 0 1 4.714667 54.72l-46.506667 66.133333-328.106667-328.064z\" /></svg>`,\n\n  overview: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M128 469.33h341.33V128H128v341.33z m85.33-256H384V384H213.33V213.33zM938.16 300.03L724.4 86.27 509.54 301.12 723.3 514.88l214.86-214.85zM724.4 206.95l93.08 93.08-94.18 94.17-93.08-93.08 94.18-94.17zM128 896h341.33V554.67H128V896z m85.33-256H384v170.67H213.33V640zM554.67 896H896V554.67H554.67V896zM640 640h170.67v170.67H640V640z\" /></svg>`,\n  profiles: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M26.1 345.1c8.4 20.1 31.5 29.7 51.6 21.3l441.1-183.3c20.1-8.4 29.7-31.5 21.3-51.6-8.4-20.1-31.5-29.7-51.6-21.3L47.4 293.4c-20.2 8.4-29.7 31.5-21.3 51.7z\" /><path d=\"M998.8 351.9c-8.4 20.1-31.5 29.7-51.6 21.3L489.5 183.1c-20.1-8.4-29.7-31.5-21.3-51.6 8.4-20.1 31.5-29.7 51.6-21.3l457.6 190.2c20.2 8.3 29.7 31.4 21.4 51.5zM556.4 888.1c-8.4 20.1-31.5 29.7-51.6 21.3L47.1 719.3c-20.1-8.4-29.7-31.5-21.3-51.6 8.4-20.1 31.5-29.7 51.6-21.3L535 836.6c20.2 8.2 29.8 31.4 21.4 51.5z\" /><path d=\"M483.9 887.9c8.4 20.1 31.5 29.7 51.6 21.3l442-183.7c20.1-8.4 29.7-31.5 21.3-51.6-8.4-20.1-31.5-29.7-51.6-21.3l-442 183.7c-20.1 8.4-29.7 31.5-21.3 51.6zM556.4 718.1c-8.4 20.1-31.5 29.7-51.6 21.3L47.1 549.3c-20.1-8.4-29.7-31.5-21.3-51.6 8.4-20.1 31.5-29.7 51.6-21.3L535 666.6c20.2 8.2 29.8 31.4 21.4 51.5z\" /><path d=\"M483.9 717.9c8.4 20.1 31.5 29.7 51.6 21.3l442-183.7c20.1-8.4 29.7-31.5 21.3-51.6-8.4-20.1-31.5-29.7-51.6-21.3l-442 183.7c-20.1 8.4-29.7 31.5-21.3 51.6zM556.4 536.1c-8.4 20.1-31.5 29.7-51.6 21.3L47.1 367.3c-20.1-8.4-29.7-31.5-21.3-51.6 8.4-20.1 31.5-29.7 51.6-21.3L535 484.6c20.2 8.2 29.8 31.4 21.4 51.5z\" /><path d=\"M483.9 535.9c8.4 20.1 31.5 29.7 51.6 21.3l442-183.7c20.1-8.4 29.7-31.5 21.3-51.6-8.4-20.1-31.5-29.7-51.6-21.3l-442 183.7c-20.1 8.4-29.7 31.5-21.3 51.6z\" /></svg>`,\n  subscriptions: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M228.9 960.4c-24.9 0-49.7-7.4-70.8-21.9-34.1-23.4-54.5-62.1-54.5-103.5V191.4C103.6 122.3 159.8 66 229 66h566.7c69.1 0 125.4 56.2 125.4 125.4V835c0 41.4-20.4 80.1-54.5 103.4-34.1 23.4-77.6 28.4-116.2 13.4l-225-87.4c-8.4-3.3-17.6-3.3-26 0l-225 87.4c-14.8 5.8-30.2 8.6-45.5 8.6z m553.7-91.9c11.2 4.4 23.4 3 33.3-3.8 9.9-6.8 15.6-17.6 15.6-29.6V191.4c0-19.8-16.1-35.9-35.9-35.9H228.9c-19.8 0-35.9 16.1-35.9 35.9V835c0 12 5.7 22.9 15.6 29.6 10 6.8 22.1 8.2 33.3 3.9l225-87.4c29.2-11.4 61.5-11.4 90.7 0l225 87.4z\" /><path d=\"M658.6 498.7H374.5c-24.7 0-44.7-20-44.7-44.7 0-24.7 20-44.7 44.7-44.7h284.1c24.7 0 44.7 20 44.7 44.7 0 24.7-20 44.7-44.7 44.7z\" /><path d=\"M516.5 640.8c-24.7 0-44.7-20-44.7-44.7V311.9c0-24.7 20-44.7 44.7-44.7 24.7 0 44.7 20 44.7 44.7V596c0 24.7-20 44.8-44.7 44.8z\" /></svg>`,\n  rulesets: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M102.4 34.133333a102.4 102.4 0 0 0-102.4 102.4v34.133334a68.266667 68.266667 0 0 0 68.266667 68.266666h102.4v546.133334H34.133333a34.133333 34.133333 0 0 0-34.133333 34.133333v68.266667a102.4 102.4 0 0 0 102.4 102.4h819.2a102.4 102.4 0 0 0 102.4-102.4v-68.266667a34.133333 34.133333 0 0 0-34.133333-34.133333h-136.533334V238.933333h102.4a68.266667 68.266667 0 0 0 68.266667-68.266666V136.533333a102.4 102.4 0 0 0-102.4-102.4H102.4z m0 68.266667h68.266667v68.266667H68.266667V136.533333a34.133333 34.133333 0 0 1 34.133333-34.133333z m136.533333 682.666667V102.4h546.133334v682.666667H238.933333zM955.733333 170.666667h-102.4V102.4h68.266667a34.133333 34.133333 0 0 1 34.133333 34.133333v34.133334zM68.266667 853.333333h887.466666v34.133334a34.133333 34.133333 0 0 1-34.133333 34.133333H102.4a34.133333 34.133333 0 0 1-34.133333-34.133333v-34.133334z m307.2-580.266666a34.133333 34.133333 0 1 0 0 68.266666h273.066666a34.133333 34.133333 0 1 0 0-68.266666h-273.066666z m0 136.533333a34.133333 34.133333 0 1 0 0 68.266667h273.066666a34.133333 34.133333 0 1 0 0-68.266667h-273.066666z m0 136.533333a34.133333 34.133333 0 1 0 0 68.266667h136.533333a34.133333 34.133333 0 1 0 0-68.266667h-136.533333z\" /></svg>`,\n  plugins: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M36.8 324.448l440.288 208.384V992L36.8 782.912V324.448z m78.016 405.76l284.256 135.008V585.6l-284.256-134.592v279.168zM512.768 46.4l477.792 209.088-477.344 225.344L36.48 253.888 512.768 46.4z m-287.36 210.272l287.68 133.344 285.152-132.288-285.632-123.84-287.2 122.784zM992 324.48v458.464l-439.488 207.872V532.832L992 324.448zM630.464 864.256l283.584-134.144v-279.008l-283.584 134.4v278.72z\" /></svg>`,\n  scheduledTasks: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M255.744 0c21.0944 0 38.1952 17.152 38.1952 38.2976v131.7888a38.2464 38.2464 0 1 1-76.4416 0V38.2976c0-21.1456 17.1008-38.2976 38.2464-38.2976zM177.8688 476.672c0-21.1456 17.1008-38.2976 38.1952-38.2976h73.8304a38.2464 38.2464 0 0 1 0 76.5952H216.064a38.2464 38.2464 0 0 1-38.1952-38.2976zM714.3424 0c21.0944 0 38.1952 17.152 38.1952 38.2976v131.7888a38.2464 38.2464 0 1 1-76.4416 0V38.2976c0-21.1456 17.152-38.2976 38.2464-38.2976zM370.3808 707.8912c0-169.2672 136.8576-306.5344 305.664-306.5344 168.96 0 305.8176 137.216 305.8176 306.5344 0 169.2672-136.9088 306.4832-305.7664 306.4832-168.8064 0-305.7152-137.216-305.7152-306.4832z m535.04 0c0-126.976-102.656-229.888-229.3248-229.888a229.5808 229.5808 0 0 0-229.2736 229.888c0 126.976 102.656 229.888 229.2736 229.888a229.5808 229.5808 0 0 0 229.3248-229.888zM177.8688 324.7104c0-21.1456 17.1008-38.2976 38.1952-38.2976h230.8096a38.2464 38.2464 0 0 1 0 76.6464H216.064a38.2464 38.2464 0 0 1-38.1952-38.3488z\" /><path d=\"M644.608 560.7424m36.5056 0l-0.0512 0q36.5056 0 36.5056 36.5056l0 122.0608q0 36.5056-36.5056 36.5056l0.0512 0q-36.5056 0-36.5056-36.5056l0-122.0608q0-36.5056 36.5056-36.5056Z\" /><path d=\"M839.68 719.3088c0 20.1728-16.384 36.5568-36.5568 36.5568h-121.3952a36.5568 36.5568 0 1 1 0-73.1648h121.3952c20.224 0 36.5568 16.384 36.5568 36.608z\" /><path d=\"M893.7472 497.7664l-76.6976-3.4304 2.2016-48.128V256.2048a76.8 76.8 0 0 0-76.8-76.8L179.2 179.2a76.8 76.8 0 0 0-76.8 76.8v563.2a76.8 76.8 0 0 0 76.8 76.8h351.0272V972.8H179.2512a153.6 153.6 0 0 1-153.6-153.6V256a153.6 153.6 0 0 1 153.6-153.6l563.2 0.2048a153.6 153.6 0 0 1 153.6 153.6v189.952l-2.304 51.6096z\" /></svg>`,\n  settings: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M305.92 810.666667H170.666667a42.666667 42.666667 0 0 1 0-85.333334h135.253333a128.042667 128.042667 0 1 1 0 85.333334z m454.826667-341.333334H853.333333a42.666667 42.666667 0 0 1 0 85.333334h-92.586666a128.042667 128.042667 0 1 1 0-85.333334zM640 554.666667a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m-128-256a42.666667 42.666667 0 0 1 0-85.333334h341.333333a42.666667 42.666667 0 0 1 0 85.333334h-341.333333z m-341.333333 256a42.666667 42.666667 0 0 1 0-85.333334h213.333333a42.666667 42.666667 0 0 1 0 85.333334H170.666667z m256 256a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334zM256 384a128 128 0 1 1 0-256 128 128 0 0 1 0 256z m0-85.333333a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z m426.666667 512a42.666667 42.666667 0 0 1 0-85.333334h170.666666a42.666667 42.666667 0 0 1 0 85.333334h-170.666666z\" fill=\"var(--primary-color)\" /></svg>`,\n  arrowDown: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M898.368 267.84l-386.432 408.512-386.368-408.512A33.472 33.472 0 0 0 100.224 256a33.472 33.472 0 0 0-25.344 11.776 39.296 39.296 0 0 0-10.88 27.52c0 10.432 3.2 19.2 9.6 26.112l411.84 434.752A34.752 34.752 0 0 0 512 768a34.752 34.752 0 0 0 26.56-11.776l411.776-433.408c6.4-7.872 9.664-17.28 9.664-28.16a37.76 37.76 0 0 0-10.88-27.52 34.56 34.56 0 0 0-25.344-11.136 33.28 33.28 0 0 0-25.344 11.776h-0.064z\" /></svg>`,\n  arrowLeft: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M675.11798 267.269064L430.406848 511.992078l244.710142 244.723015a38.453744 38.453744 0 0 1-54.380252 54.383222l-271.900267-271.91116a38.455724 38.455724 0 0 1 0-54.382232l271.900267-271.91116a38.453744 38.453744 0 0 1 54.381242 54.375301z\" /></svg>`,\n  arrowRight: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M675.11699 539.183194l-271.900268 271.91116a38.453744 38.453744 0 0 1-54.380251-54.383222l244.710141-244.723014-244.710141-244.719054a38.453744 38.453744 0 0 1 54.380251-54.383222l271.900268 271.91116a38.455724 38.455724 0 0 1 0 54.386192z\" /></svg>`,\n  clear3: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M837.632 915.456H705.536c-12.8 0-23.04-10.24-23.04-23.04v-58.368c0-4.096-8.704-11.776-23.04-11.776s-23.04 7.68-23.04 11.776v58.368c0 12.8-10.24 23.04-23.04 23.04h-158.72c-24.064-19.968-9.728-46.592 10.752-46.592h123.904v-34.816c0-32.768 30.72-58.368 69.632-58.368s69.632 25.6 69.632 58.368v34.816h85.504v-232.96H209.408v232.96h92.672c12.288 0 23.04 8.704 24.064 20.992 1.536 13.824-9.728 25.6-23.04 25.6H186.368c-12.8 0-23.04-10.24-23.04-23.04v-279.04c0-12.8 10.24-23.04 23.04-23.04h651.776c12.8 0 23.04 10.24 23.04 23.04v279.04c0 12.8-10.752 23.04-23.552 23.04z\" /><path d=\"M837.632 636.416H186.368c-12.8 0-23.04-10.24-23.04-23.04v-69.632c0-11.264 8.192-20.992 19.456-23.04l244.736-43.008v-353.28c0-26.112 24.064-46.592 54.272-46.592h61.952c30.72 0 54.272 20.48 54.272 46.592v352.768l244.736 43.008c11.264 2.048 19.456 11.776 19.456 23.04v69.632c-1.024 12.8-11.264 23.04-24.576 23.552z m-628.224-46.592h605.184v-27.136L569.856 519.68c-11.264-2.048-19.456-11.264-19.456-23.04V125.952c-2.048-1.536-5.12-2.048-7.68-2.048h-61.952c-3.584 0-6.656 1.024-7.68 2.048V496.64c0 11.264-8.192 20.992-19.456 23.04L209.408 563.2v26.624zM488.96 915.456H302.08c-12.8 0-23.04-10.24-23.04-23.04s10.24-23.04 23.04-23.04h186.88c12.8 0 23.04 10.24 23.04 23.04s-10.24 23.04-23.04 23.04z\" /><path d=\"M162.816 589.824h698.368v46.592H162.816z\" /></svg>`,\n  disabled: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M272.298667 812.032l-1.664 1.706667-60.330667-60.373334 1.664-1.664a384.042667 384.042667 0 0 1 539.733333-539.733333l1.664-1.706667 60.330667 60.373334-1.664 1.664a384.042667 384.042667 0 0 1-539.733333 539.733333z m0.469333-121.173333l418.133333-418.090667a298.752 298.752 0 0 0-418.133333 418.133333z m478.464-357.76l-418.133333 418.133333a298.752 298.752 0 0 0 418.133333-418.133333z\" /></svg>`,\n  empty: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M831.7 369.4H193.6L64 602v290.3h897.2V602L831.7 369.4zM626.6 604.6c0 62.9-51 113.9-114 113.9s-114-51-114-113.9H117.5l103.8-198h582.5l103.8 198h-281zM502.2 131h39.1v140.6h-39.1zM236.855 200.802l27.647-27.647 99.419 99.418-27.648 27.648zM667.547 272.637l99.418-99.419 27.648 27.648-99.418 99.418z\" /></svg>`,\n  reset: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M624.593455 23.272727a93.090909 93.090909 0 0 1 93.090909 93.090909v168.587637l143.406545 0.023272a116.363636 116.363636 0 0 1 116.247273 111.313455l0.116363 5.050182V861.090909a116.363636 116.363636 0 0 1-116.363636 116.363636H162.909091a116.363636 116.363636 0 0 1-116.363636-116.363636V401.338182a116.363636 116.363636 0 0 1 116.363636-116.363637l146.664727-0.023272V116.363636a93.090909 93.090909 0 0 1 88.459637-92.974545l4.654545-0.116364zM139.636364 581.818182v279.272727a23.272727 23.272727 0 0 0 23.272727 23.272727h302.545454v-162.909091a46.545455 46.545455 0 1 1 93.09091 0v162.909091h93.090909v-162.909091a46.545455 46.545455 0 1 1 93.090909 0v162.909091h116.363636a23.272727 23.272727 0 0 0 23.272727-23.272727V581.818182H139.636364z m0-93.090909h744.727272v-87.389091a23.272727 23.272727 0 0 0-23.272727-23.272727h-166.679273a69.818182 69.818182 0 0 1-69.818181-69.818182V116.363636h-221.905455v191.883637a69.818182 69.818182 0 0 1-69.818182 69.818182H162.909091a23.272727 23.272727 0 0 0-23.272727 23.272727V488.727273z\" /></svg>`,\n  telegram: `<svg viewBox=\"0 0 1024 1024\"><path d=\"M417.28 795.733333 429.226667 615.253333 756.906667 320C771.413333 306.773333 753.92 300.373333 734.72 311.893333L330.24 567.466667 155.306667 512C117.76 501.333333 117.333333 475.306667 163.84 456.533333L845.226667 193.706667C876.373333 179.626667 906.24 201.386667 894.293333 249.173333L778.24 795.733333C770.133333 834.56 746.666667 843.946667 714.24 826.026667L537.6 695.466667 452.693333 777.813333C442.88 787.626667 434.773333 795.733333 417.28 795.733333Z\" /></svg>`,\n\n  copy: `<svg viewBox=\"0 0 24 24\"><path d=\"M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z\" /></svg>`,\n  paste: `<svg viewBox=\"0 0 24 24\"><path d=\"M19,20H5V4H7V7H17V4H19M12,2A1,1 0 0,1 13,3A1,1 0 0,1 12,4A1,1 0 0,1 11,3A1,1 0 0,1 12,2M19,2H14.82C14.4,0.84 13.3,0 12,0C10.7,0 9.6,0.84 9.18,2H5A2,2 0 0,0 3,4V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V4A2,2 0 0,0 19,2Z\" /></svg>`,\n  link: `<svg viewBox=\"0 0 24 24\"><path d=\"M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z\" /></svg>`,\n  loading: `<svg viewBox=\"0 0 24 24\"><path d=\"M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z\" /></svg>`,\n  log: `<svg viewBox=\"0 0 24 24\"><path d=\"M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z\" fill=\"var(--primary-color)\" /></svg>`,\n  minimize: `<svg viewBox=\"0 0 24 24\"><path d=\"M20,14H4V10H20\" /></svg>`,\n  maximize: `<svg viewBox=\"0 0 24 24\"><path d=\"M4,4H20V20H4V4M6,8V18H18V8H6Z\" /></svg>`,\n  maximize2: `<svg viewBox=\"0 0 24 24\"><path d=\"M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z\" /></svg>`,\n  more: `<svg viewBox=\"0 0 24 24\"><path d=\"M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z\" /></svg>`,\n  pinFill: `<svg viewBox=\"0 0 24 24\"><path d=\"M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z\" /></svg>`,\n  pin: `<svg viewBox=\"0 0 24 24\"><path d=\"M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12M8.8,14L10,12.8V4H14V12.8L15.2,14H8.8Z\" /></svg>`,\n  preview: `<svg viewBox=\"0 0 24 24\"><path d=\"M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9M12,4.5C17,4.5 21.27,7.61 23,12C21.27,16.39 17,19.5 12,19.5C7,19.5 2.73,16.39 1,12C2.73,7.61 7,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C15.76,17.5 19.17,15.36 20.82,12C19.17,8.64 15.76,6.5 12,6.5C8.24,6.5 4.83,8.64 3.18,12Z\" /></svg>`,\n  refresh: `<svg viewBox=\"0 0 24 24\"><path d=\"M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z\" /></svg>`,\n  restartApp: `<svg viewBox=\"0 0 24 24\"><path d=\"M8 17V15H16V17H8M16 10L12 6L8 10H10.5V14H13.5V10H16M12 2C17.5 2 22 6.5 22 12C22 17.5 17.5 22 12 22C6.5 22 2 17.5 2 12C2 6.5 6.5 2 12 2M12 4C7.58 4 4 7.58 4 12C4 16.42 7.58 20 12 20C16.42 20 20 16.42 20 12C20 7.58 16.42 4 12 4Z\" /></svg>`,\n  restart: `<svg viewBox=\"0 0 24 24\"><path d=\"M12,4C14.1,4 16.1,4.8 17.6,6.3C20.7,9.4 20.7,14.5 17.6,17.6C15.8,19.5 13.3,20.2 10.9,19.9L11.4,17.9C13.1,18.1 14.9,17.5 16.2,16.2C18.5,13.9 18.5,10.1 16.2,7.7C15.1,6.6 13.5,6 12,6V10.6L7,5.6L12,0.6V4M6.3,17.6C3.7,15 3.3,11 5.1,7.9L6.6,9.4C5.5,11.6 5.9,14.4 7.8,16.2C8.3,16.7 8.9,17.1 9.6,17.4L9,19.4C8,19 7.1,18.4 6.3,17.6Z\" fill=\"var(--primary-color)\" /></svg>`,\n  rollback: `<svg viewBox=\"0 0 24 24\"><path d=\"M20 13.5C20 17.09 17.09 20 13.5 20H6V18H13.5C16 18 18 16 18 13.5S16 9 13.5 9H7.83L10.91 12.09L9.5 13.5L4 8L9.5 2.5L10.92 3.91L7.83 7H13.5C17.09 7 20 9.91 20 13.5Z\" /></svg>`,\n  selected: `<svg viewBox=\"0 0 24 24\"><path d=\"M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z\" /></svg>`,\n  settings2: `<svg viewBox=\"0 0 24 24\"><path d=\"M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z\" /></svg>`,\n  settings3: `<svg viewBox=\"0 0 24 24\"><path d=\"M8 12.14V2H6V12.14C4.28 12.59 3 14.14 3 16S4.28 19.41 6 19.86V22H8V19.86C9.72 19.41 11 17.86 11 16S9.72 12.59 8 12.14M7 14C8.1 14 9 14.9 9 16S8.1 18 7 18C5.9 18 5 17.1 5 16S5.9 14 7 14M18 2H16V4.14C14.28 4.59 13 6.14 13 8S14.28 11.41 16 11.86V22H18V11.86C19.72 11.41 21 9.86 21 8S19.72 4.59 18 4.14V2M17 6C18.1 6 19 6.9 19 8S18.1 10 17 10C15.9 10 15 9.1 15 8S15.9 6 17 6Z\" /></svg>`,\n  sparkle: `<svg viewBox=\"0 0 24 24\"><path d=\"M9 4L11.5 9.5L17 12L11.5 14.5L9 20L6.5 14.5L1 12L6.5 9.5L9 4M9 8.83L8 11L5.83 12L8 13L9 15.17L10 13L12.17 12L10 11L9 8.83M19 9L17.74 6.26L15 5L17.74 3.75L19 1L20.25 3.75L23 5L20.25 6.26L19 9M19 23L17.74 20.26L15 19L17.74 17.75L19 15L20.25 17.75L23 19L20.25 20.26L19 23Z\" /></svg>`,\n  speedTest: `<svg viewBox=\"0 0 24 24\"><path d=\"M12 1.38L9.14 12.06C8.8 13.1 9.04 14.29 9.86 15.12C11.04 16.29 12.94 16.29 14.11 15.12C14.9 14.33 15.16 13.2 14.89 12.21M14.6 3.35L15.22 5.68C18.04 6.92 20 9.73 20 13C20 15.21 19.11 17.21 17.66 18.65H17.65C17.26 19.04 17.26 19.67 17.65 20.06C18.04 20.45 18.68 20.45 19.07 20.07C20.88 18.26 22 15.76 22 13C22 8.38 18.86 4.5 14.6 3.35M9.4 3.36C5.15 4.5 2 8.4 2 13C2 15.76 3.12 18.26 4.93 20.07C5.32 20.45 5.95 20.45 6.34 20.06C6.73 19.67 6.73 19.04 6.34 18.65C4.89 17.2 4 15.21 4 13C4 9.65 5.94 6.86 8.79 5.65\" /></svg>`,\n  stop: `<svg viewBox=\"0 0 24 24\"><path d=\"M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M9,9V15H15V9\" fill=\"var(--primary-color)\" /></svg>`,\n  add: `<svg viewBox=\"0 0 24 24\"><path d=\"M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z\" /></svg>`,\n  clear2: `<svg viewBox=\"0 0 24 24\"><path d=\"M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z\" /></svg>`,\n  close: `<svg viewBox=\"0 0 24 24\"><path d=\"M13.46,12L19,17.54V19H17.54L12,13.46L6.46,19H5V17.54L10.54,12L5,6.46V5H6.46L12,10.54L17.54,5H19V6.46L13.46,12Z\" /></svg>`,\n  code: `<svg viewBox=\"0 0 24 24\"><path d=\"M12.89,3L14.85,3.4L11.11,21L9.15,20.6L12.89,3M19.59,12L16,8.41V5.58L22.42,12L16,18.41V15.58L19.59,12M1.58,12L8,5.58V8.41L4.41,12L8,15.58V18.41L1.58,12Z\" /></svg>`,\n  collapse: `<svg viewBox=\"0 0 24 24\"><path d=\"M16.59,5.41L15.17,4L12,7.17L8.83,4L7.41,5.41L12,10M7.41,18.59L8.83,20L12,16.83L15.17,20L16.58,18.59L12,14L7.41,18.59Z\" /></svg>`,\n  delete: `<svg viewBox=\"0 0 24 24\"><path d=\"M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z\" /></svg>`,\n  drag: `<svg viewBox=\"0 0 24 24\"><path d=\"M7,19V17H9V19H7M11,19V17H13V19H11M15,19V17H17V19H15M7,15V13H9V15H7M11,15V13H13V15H11M15,15V13H17V15H15M7,11V9H9V11H7M11,11V9H13V11H11M15,11V9H17V11H15M7,7V5H9V7H7M11,7V5H13V7H11M15,7V5H17V7H15Z\" /></svg>`,\n  edit: `<svg viewBox=\"0 0 24 24\"><path d=\"M5,3C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19H5V5H12V3H5M17.78,4C17.61,4 17.43,4.07 17.3,4.2L16.08,5.41L18.58,7.91L19.8,6.7C20.06,6.44 20.06,6 19.8,5.75L18.25,4.2C18.12,4.07 17.95,4 17.78,4M15.37,6.12L8,13.5V16H10.5L17.87,8.62L15.37,6.12Z\" /></svg>`,\n  error: `<svg viewBox=\"0 0 24 24\"><path d=\"M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z\" /></svg>`,\n  expand: `<svg viewBox=\"0 0 24 24\"><path d=\"M12,18.17L8.83,15L7.42,16.41L12,21L16.59,16.41L15.17,15M12,5.83L15.17,9L16.58,7.59L12,3L7.41,7.59L8.83,9L12,5.83Z\" /></svg>`,\n  file: `<svg viewBox=\"0 0 24 24\"><path d=\"M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z\" /></svg>`,\n  filter: `<svg viewBox=\"0 0 24 24\"><path d=\"M15,19.88C15.04,20.18 14.94,20.5 14.71,20.71C14.32,21.1 13.69,21.1 13.3,20.71L9.29,16.7C9.06,16.47 8.96,16.16 9,15.87V10.75L4.21,4.62C3.87,4.19 3.95,3.56 4.38,3.22C4.57,3.08 4.78,3 5,3V3H19V3C19.22,3 19.43,3.08 19.62,3.22C20.05,3.56 20.13,4.19 19.79,4.62L15,10.75V19.88M7.04,5L11,10.06V15.58L13,17.58V10.05L16.96,5H7.04Z\" /></svg>`,\n  folder: `<svg viewBox=\"0 0 24 24\"><path d=\"M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z\" /></svg>`,\n  forbidden: `<svg viewBox=\"0 0 24 24\"><path d=\"M12 2C17.5 2 22 6.5 22 12S17.5 22 12 22 2 17.5 2 12 6.5 2 12 2M12 4C10.1 4 8.4 4.6 7.1 5.7L18.3 16.9C19.3 15.5 20 13.8 20 12C20 7.6 16.4 4 12 4M16.9 18.3L5.7 7.1C4.6 8.4 4 10.1 4 12C4 16.4 7.6 20 12 20C13.9 20 15.6 19.4 16.9 18.3Z\" /></svg>`,\n  backward: `<svg viewBox=\"0 0 24 24\"><path d=\"M10,9.9L7,12L10,14.1V9.9M19,9.9L16,12L19,14.1V9.9M12,6V18L3.5,12L12,6M21,6V18L12.5,12L21,6Z\" /></svg>`,\n  pause: `<svg viewBox=\"0 0 24 24\"><path d=\"M14,19H18V5H14M6,19H10V5H6V19Z\" /></svg>`,\n  play: `<svg viewBox=\"0 0 24 24\"><path d=\"M8,5.14V19.14L19,12.14L8,5.14Z\" /></svg>`,\n  forward: `<svg viewBox=\"0 0 24 24\"><path d=\"M15,9.9L18,12L15,14.1V9.9M6,9.9L9,12L6,14.1V9.9M13,6V18L21.5,12L13,6M4,6V18L12.5,12L4,6Z\" /></svg>`,\n  github: `<svg viewBox=\"0 0 24 24\"><path d=\"M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z\" /></svg>`,\n  grant: `<svg viewBox=\"0 0 24 24\"><path d=\"M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21M14.8,11V9.5C14.8,8.1 13.4,7 12,7C10.6,7 9.2,8.1 9.2,9.5V11C8.6,11 8,11.6 8,12.2V15.7C8,16.4 8.6,17 9.2,17H14.7C15.4,17 16,16.4 16,15.8V12.3C16,11.6 15.4,11 14.8,11M13.5,11H10.5V9.5C10.5,8.7 11.2,8.2 12,8.2C12.8,8.2 13.5,8.7 13.5,9.5V11Z\" /></svg>`,\n}\n\nexport type IconName = keyof typeof icons\n"
  },
  {
    "path": "frontend/src/components/Icon/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { icons, type IconName } from './icons'\n\ninterface Props {\n  icon: IconName\n  size?: number\n  color?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), { size: 16, color: 'var(--color)' })\n\nconst sizeWithUnit = computed(() => props.size + 'px')\n</script>\n\n<template>\n  <div class=\"inline-flex\">\n    <span class=\"icon flex items-center justify-center\" v-html=\"icons[icon] || icons.error\" />\n  </div>\n</template>\n\n<style>\n.icon svg {\n  width: v-bind(sizeWithUnit);\n  height: v-bind(sizeWithUnit);\n  fill: v-bind(color);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Input/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue'\n\nimport useI18n from '@/lang'\nimport { debounce } from '@/utils'\nimport { ClipboardGetText } from '@/bridge'\n\nexport interface Props {\n  modelValue?: string | number | undefined\n  autoSize?: boolean\n  placeholder?: string\n  type?: 'number' | 'text' | 'code'\n  lang?: 'yaml' | 'json' | 'javascript'\n  size?: 'default' | 'small'\n  editable?: boolean\n  clearable?: boolean\n  allowPaste?: boolean\n  autofocus?: boolean\n  min?: number\n  max?: number\n  disabled?: boolean\n  border?: boolean\n  delay?: number\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  modelValue: '',\n  autoSize: false,\n  placeholder: undefined,\n  type: 'text',\n  lang: 'javascript',\n  size: 'default',\n  editable: false,\n  autofocus: false,\n  allowPaste: false,\n  min: undefined,\n  max: undefined,\n  clearable: false,\n  disabled: false,\n  border: true,\n  delay: 0,\n})\n\nconst emits = defineEmits(['change', 'update:modelValue', 'submit'])\n\nconst showEdit = ref(false)\nconst inputRef = useTemplateRef('inputRef')\nconst innerClearable = computed(\n  () => props.clearable && props.type !== 'code' && props.modelValue && !props.disabled,\n)\nconst innerAllowPaste = computed(() => props.allowPaste && props.type !== 'code' && !props.disabled)\n\nconst { t } = useI18n.global\n\nconst validate = (val: string | number) => {\n  if (props.type === 'number') {\n    val = Number(val)\n    if (Number.isNaN(val)) {\n      throw new Error('Please enter a valid number')\n    }\n    const { min, max } = props\n    if (min !== undefined) {\n      val = val < min ? min : val\n    }\n    if (max !== undefined) {\n      val = val > max ? max : val\n    }\n  }\n  return val\n}\n\nconst onInput = debounce((e: any) => {\n  const val = validate(e.target.value)\n  e.target.value = val\n  emits('update:modelValue', val)\n  emits('change', val)\n}, props.delay)\n\nconst handleClear = () => {\n  const val = props.type === 'number' ? Math.min(props.min || 0, 0) : ''\n  emits('update:modelValue', val)\n  emits('change', val)\n  !props.editable && nextTick(() => inputRef.value?.focus())\n}\n\nconst handlePaste = async () => {\n  const text = await ClipboardGetText()\n  const val = validate(text)\n  emits('update:modelValue', val)\n  emits('change', val)\n}\n\nconst showInput = () => {\n  if (props.disabled) return\n  showEdit.value = true\n  nextTick(() => inputRef.value?.focus())\n}\n\nconst onSubmit = debounce(\n  (e: any) => {\n    const val = validate(e.target.value)\n    e.target.value = val\n    emits('submit', val)\n    props.editable && (showEdit.value = false)\n  },\n  props.clearable ? 100 : 0,\n)\n\nonMounted(() => props.autofocus && !props.editable && inputRef.value?.focus())\n\ndefineExpose({\n  focus: () => inputRef.value?.focus(),\n})\n</script>\n\n<template>\n  <div\n    v-bind=\"$attrs\"\n    :class=\"{\n      border: border && (!editable || showEdit),\n      'auto-size': autoSize,\n      'bg-color': !editable || showEdit,\n      'is-editable': editable && !showEdit,\n      [size]: true,\n      disabled,\n    }\"\n    :style=\"{\n      height: type === 'code' ? '' : size === 'small' ? '26px' : '30px',\n    }\"\n    class=\"gui-input inline-flex items-center rounded-4 cursor-pointer px-4\"\n  >\n    <div v-if=\"$slots.prefix\" class=\"flex items-center shrink-0\">\n      <slot name=\"prefix\" v-bind=\"{ showInput }\"></slot>\n    </div>\n    <Icon v-if=\"disabled\" icon=\"forbidden\" class=\"shrink-0\" />\n    <div\n      v-if=\"editable && !showEdit\"\n      class=\"w-full overflow-hidden whitespace-nowrap text-ellipsis\"\n      :class=\"{ 'italic pr-4': !modelValue }\"\n      @click=\"showInput\"\n    >\n      <slot name=\"editable\" v-bind=\"{ value: modelValue }\">\n        {{ modelValue || t(placeholder || 'common.none') }}\n      </slot>\n    </div>\n    <template v-else>\n      <CodeViewer\n        v-if=\"type === 'code'\"\n        :value=\"modelValue\"\n        :lang=\"lang\"\n        :editable=\"!disabled\"\n        :placeholder=\"placeholder\"\n        class=\"code w-full overflow-y-auto\"\n        @change=\"(value: string) => onInput({ target: { value } })\"\n      />\n      <input\n        v-else\n        ref=\"inputRef\"\n        :value=\"modelValue\"\n        :placeholder=\"placeholder\"\n        :type=\"type\"\n        :disabled=\"disabled\"\n        autocomplete=\"off\"\n        class=\"flex-1 inline-block py-6 outline-none border-0 bg-transparent w-0\"\n        @input=\"onInput\"\n        @blur=\"onSubmit\"\n        @keydown.enter=\"() => nextTick(() => inputRef?.blur())\"\n        @keydown.esc.stop.prevent=\"inputRef?.blur\"\n      />\n      <Button v-if=\"innerClearable\" icon=\"clear2\" type=\"text\" size=\"small\" @click=\"handleClear\" />\n      <Button v-if=\"innerAllowPaste\" icon=\"paste\" type=\"text\" size=\"small\" @click=\"handlePaste\" />\n    </template>\n    <div v-if=\"$slots.suffix\" class=\"flex items-center shrink-0\">\n      <slot name=\"suffix\" v-bind=\"{ showInput }\"></slot>\n    </div>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-input {\n  min-width: 220px;\n  border: 1px solid transparent;\n  input {\n    color: var(--input-color);\n  }\n  .code {\n    max-height: 300px;\n  }\n}\n\n.bg-color {\n  background: var(--input-bg);\n}\n\n.is-editable {\n  min-width: 0;\n  max-width: 220px;\n}\n\n.auto-size {\n  min-width: 0 !important;\n}\n\n.disabled {\n  cursor: not-allowed;\n  input {\n    cursor: not-allowed;\n  }\n}\n\n.border {\n  border: 1px solid var(--primary-color);\n}\n\n.small {\n  input {\n    font-size: 12px;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/InputList/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, useTemplateRef, watch } from 'vue'\n\nimport { DraggableOptions } from '@/constant/app'\nimport { sampleID } from '@/utils'\n\nimport Input from '@/components/Input/index.vue'\n\ninterface Props {\n  modelValue?: string[]\n  placeholder?: string\n  autofocus?: boolean\n}\n\ninterface Item {\n  value: string\n  id: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  modelValue: () => [],\n  placeholder: '',\n  autofocus: true,\n})\n\nconst emit = defineEmits(['change', 'update:modelValue'])\n\nconst innerList = ref<Item[]>(props.modelValue.map((v, i) => ({ value: v, id: i.toString() })))\n\nconst editItem = ref<Item>()\nconst inputVal = ref('')\nconst inputRef = useTemplateRef<typeof Input>('inputRef')\n\nconst handleAdd = () => {\n  const item = editItem.value\n  editItem.value = undefined\n  if (!inputVal.value) return\n  if (item) {\n    item.value = inputVal.value\n  } else {\n    innerList.value.push({ value: inputVal.value, id: sampleID() })\n  }\n  inputVal.value = ''\n  inputRef.value?.focus()\n  emitUpdate()\n}\n\nconst handleEdit = (item: Item) => {\n  editItem.value = item\n  inputVal.value = editItem.value.value\n  inputRef.value?.focus()\n}\n\nconst handleDel = (item: Item) => {\n  const idx = innerList.value.indexOf(item)\n  if (idx !== -1) {\n    innerList.value.splice(idx, 1)\n    emitUpdate()\n  }\n}\n\nlet internalUpdate = false\n\nwatch(\n  () => props.modelValue,\n  (val) => {\n    if (!internalUpdate) {\n      innerList.value.splice(0)\n      innerList.value.push(...val.map((v, i) => ({ value: v, id: i.toString() })))\n    }\n    internalUpdate = false\n  },\n  {\n    deep: true,\n  },\n)\n\nconst emitUpdate = () => {\n  internalUpdate = true\n  const list = innerList.value.map((v) => v.value)\n  emit('update:modelValue', list)\n  emit('change', list)\n}\n</script>\n\n<template>\n  <div class=\"gui-input-list inline-block rounded-4\">\n    <div\n      v-draggable=\"[innerList, { ...DraggableOptions, onUpdate: emitUpdate }]\"\n      class=\"flex flex-col gap-2\"\n    >\n      <TransitionGroup name=\"list\">\n        <Card v-for=\"item in innerList\" :key=\"item.id\">\n          <div class=\"flex items-center py-4 break-all\">\n            <span class=\"mr-auto\">{{ item.value }}</span>\n            <Button\n              icon=\"edit\"\n              :icon-size=\"14\"\n              size=\"small\"\n              type=\"text\"\n              @click=\"handleEdit(item)\"\n            />\n            <Button\n              icon=\"close\"\n              :icon-size=\"14\"\n              size=\"small\"\n              type=\"text\"\n              @click=\"handleDel(item)\"\n            />\n          </div>\n        </Card>\n      </TransitionGroup>\n    </div>\n\n    <Input\n      ref=\"inputRef\"\n      v-model=\"inputVal\"\n      :placeholder=\"placeholder\"\n      type=\"text\"\n      clearable\n      :autofocus=\"autofocus\"\n      class=\"mt-4 w-full\"\n      @keydown.enter=\"handleAdd\"\n    >\n      <template #suffix>\n        <Button :icon=\"editItem ? 'edit' : 'add'\" size=\"small\" type=\"primary\" @click=\"handleAdd\" />\n      </template>\n    </Input>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-input-list {\n  min-width: 220px;\n}\n.list-enter-active,\n.list-leave-active {\n  transition: all 0.2s ease;\n}\n.list-enter-from,\n.list-leave-to {\n  opacity: 0;\n  transform: scale(0);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/InterfaceSelect/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref } from 'vue'\n\nimport { GetInterfaces } from '@/bridge'\n\ninterface Props {\n  border?: boolean\n}\n\nconst model = defineModel<string>()\n\nwithDefaults(defineProps<Props>(), {\n  border: true,\n})\n\nconst emits = defineEmits(['change'])\n\nconst options = ref<any>([])\n\nconst onChange = (val: string) => {\n  emits('change', val)\n}\n\nGetInterfaces().then((res) => {\n  options.value = [\n    {\n      label: 'common.auto',\n      value: '',\n    },\n    ...res.map((v) => ({ label: v, value: v })),\n  ]\n})\n</script>\n\n<template>\n  <Select v-model=\"model\" v-bind=\"$attrs\" :options=\"options\" :border=\"border\" @change=\"onChange\" />\n</template>\n"
  },
  {
    "path": "frontend/src/components/KeyValueEditor/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, watch } from 'vue'\n\ninterface Props {\n  modelValue?: Recordable\n  placeholder?: [string, string]\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  modelValue: () => ({}),\n  placeholder: () => ['key', 'value'],\n})\n\nconst emit = defineEmits(['change', 'update:modelValue'])\n\nconst entries = ref(Object.entries(props.modelValue))\n\nconst handleDel = (i: number) => {\n  entries.value.splice(i, 1)\n  emitUpdate()\n}\n\nconst handleAdd = () => {\n  entries.value.push(['', ''])\n  emitUpdate()\n}\n\nlet internalUpdate = false\n\nwatch(\n  () => props.modelValue,\n  (val) => {\n    if (!internalUpdate) {\n      entries.value = Object.entries(val)\n    }\n    internalUpdate = false\n  },\n  { deep: true },\n)\n\nconst emitUpdate = () => {\n  const obj = Object.fromEntries(entries.value)\n  if (!internalUpdate) {\n    emit('update:modelValue', obj)\n  }\n  emit('change', obj)\n  internalUpdate = true\n}\n</script>\n\n<template>\n  <div class=\"gui-kv-editor inline-flex flex-col\">\n    <div v-for=\"(entry, i) in entries\" :key=\"i\" class=\"flex items-center mb-4\">\n      <Input\n        v-model=\"entry[0]\"\n        :placeholder=\"placeholder[0]\"\n        auto-size\n        class=\"flex-1\"\n        @submit=\"emitUpdate\"\n      />\n      <Button type=\"text\" :icon-size=\"12\" icon=\"close\" @click=\"handleDel(i)\" />\n      <Input\n        v-model=\"entry[1]\"\n        :placeholder=\"placeholder[1]\"\n        auto-size\n        class=\"flex-1\"\n        @submit=\"emitUpdate\"\n      />\n    </div>\n    <Button type=\"primary\" icon=\"add\" @click=\"handleAdd\" />\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-kv-editor {\n  min-width: 400px;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Menu/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { onMounted, onUnmounted, ref, watch, nextTick, useTemplateRef } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport type { Menu } from '@/types/app'\n\ninterface Props {\n  position: { x: number; y: number }\n  menuList: Menu[]\n}\n\nconst model = defineModel<boolean>({ default: false })\nconst props = defineProps<Props>()\n\nconst secondaryMenu = ref<Menu[] | undefined>()\n\nconst menuRef = useTemplateRef('menuRef')\nconst secondaryMenuRef = useTemplateRef('secondaryMenuRef')\n\nconst menuPosition = ref({ left: '', top: '' })\nconst secondaryMenuPosition = ref({ left: '', top: '' })\n\nconst { t } = useI18n()\n\nconst handleClick = (fn: Menu) => {\n  fn.handler?.()\n  model.value = false\n  secondaryMenu.value = undefined\n}\n\nconst fixMenuPos = (x: number, y: number) => {\n  let left = x\n  let top = y\n\n  const { offsetWidth: clientWidth, offsetHeight: clientHeight } = document.body\n  const { offsetWidth: menuWidth, offsetHeight: menuHeight } = menuRef.value!\n\n  if (x + menuWidth > clientWidth) left -= x + menuWidth - clientWidth + 8\n  if (y + menuHeight > clientHeight) top -= y + menuHeight - clientHeight + 8\n\n  menuPosition.value = { left: left + 'px', top: top + 'px' }\n}\n\nconst fixSecondaryMenuPos = () => {\n  const { x, y } = props.position\n  const { offsetWidth: menuWidth, offsetHeight: menuHeight } = menuRef.value!\n\n  let left = menuWidth\n  let top = menuHeight\n\n  const { offsetWidth: clientWidth, offsetHeight: clientHeight } = document.body\n  const { offsetWidth: sMenuWidth, offsetHeight: sMenuHeight } = secondaryMenuRef.value!\n\n  if (left + sMenuWidth + x > clientWidth) left -= x + menuWidth + sMenuWidth - clientWidth + 8\n  if (top + sMenuHeight + y > clientHeight) top -= sMenuHeight\n\n  secondaryMenuPosition.value = { left: left + 'px', top: top + 'px' }\n}\n\nwatch(\n  () => props.position,\n  ({ x, y }) => {\n    nextTick(() => fixMenuPos(x, y))\n    secondaryMenu.value = undefined\n  },\n)\n\nwatch([() => secondaryMenu.value, () => props.position], () => {\n  nextTick(fixSecondaryMenuPos)\n})\n\nconst onClick = () => {\n  model.value = false\n  secondaryMenu.value = undefined\n}\n\nonMounted(() => document.addEventListener('click', onClick))\nonUnmounted(() => document.removeEventListener('click', onClick))\n</script>\n\n<template>\n  <Transition name=\"menu\">\n    <div\n      v-show=\"model\"\n      ref=\"menuRef\"\n      :style=\"menuPosition\"\n      class=\"gui-menu fixed z-9999 p-4 rounded-6 shadow flex flex-col gap-4 backdrop-blur-sm\"\n    >\n      <template v-for=\"menu in menuList\">\n        <Divider v-if=\"menu.separator\" :key=\"menu.label + '_divider'\">{{ t(menu.label) }}</Divider>\n        <Button\n          v-else\n          :key=\"menu.label\"\n          type=\"text\"\n          size=\"small\"\n          @click=\"handleClick(menu)\"\n          @mouseenter=\"secondaryMenu = menu.children\"\n        >\n          <div class=\"text-nowrap\">\n            {{ t(menu.label) }}\n          </div>\n          <Icon v-if=\"menu.children\" icon=\"arrowRight\" class=\"ml-8\" />\n        </Button>\n      </template>\n      <Transition name=\"menu\">\n        <div\n          v-show=\"secondaryMenu\"\n          ref=\"secondaryMenuRef\"\n          :style=\"secondaryMenuPosition\"\n          class=\"gui-menu absolute fixed z-999 p-4 rounded-6 shadow flex flex-col gap-4 backdrop-blur-sm\"\n        >\n          <Button\n            v-for=\"m in secondaryMenu\"\n            :key=\"m.label\"\n            type=\"text\"\n            size=\"small\"\n            @click.stop=\"handleClick(m)\"\n          >\n            <Divider v-if=\"m.separator\" :key=\"m.label + '_divider'\" size=\"small\">\n              {{ t(m.label) }}\n            </Divider>\n            <div v-else class=\"text-nowrap\">{{ t(m.label) }}</div>\n          </Button>\n        </div>\n      </Transition>\n    </div>\n  </Transition>\n</template>\n\n<style lang=\"less\" scoped>\n.menu-enter-active,\n.menu-leave-active {\n  transition:\n    transform 0.2s ease-in-out,\n    opacity 0.2s ease-in-out;\n  transform-origin: top;\n}\n\n.menu-enter-from,\n.menu-leave-to {\n  opacity: 0;\n  transform: scaleY(0);\n}\n\n.gui-menu {\n  background: var(--menu-bg);\n  min-width: 90px;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Message/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport i18n from '@/lang'\n\nexport type MessageIcon = 'info' | 'warn' | 'error' | 'success'\n\ninterface Props {\n  icon?: MessageIcon\n  content: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  icon: 'info',\n})\n\ndefineEmits(['close'])\n\nconst { t } = i18n.global\n\nconst iconMap = {\n  info: 'messageInfo',\n  success: 'messageSuccess',\n  error: 'messageError',\n  warn: 'messageWarn',\n}\n\nconst icon = computed(() => iconMap[props.icon] as any)\n</script>\n\n<template>\n  <Transition name=\"slide-down\" appear>\n    <div class=\"gui-message flex items-center p-8 pl-16 rounded-8 my-4 shadow\">\n      <Icon class=\"shrink-0\" :icon=\"icon\" />\n      <div class=\"text-14 pl-12 break-all\">{{ t(content) }}</div>\n      <Button\n        icon=\"close\"\n        :icon-size=\"10\"\n        type=\"text\"\n        size=\"small\"\n        class=\"close px-4 invisible\"\n        @click=\"$emit('close')\"\n      />\n    </div>\n  </Transition>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-message {\n  background: var(--toast-bg);\n  &:hover {\n    .close {\n      visibility: visible;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Modal/index.ts",
    "content": "import { ref, defineComponent, h, computed, type VNode, type ComponentPublicInstance } from 'vue'\n\nimport Modal from './index.vue'\n\nimport type { Props as ModalProps, Slots as ModalSlots } from './index.vue'\n\nexport const useModal = (options: Partial<ModalProps>, contents: ModalSlots = {}) => {\n  const open = ref(false)\n  const props = ref(options)\n  const slots = ref(contents)\n\n  if ('component' in options && options.component) {\n    console.warn(\n      '[Deprecated] The \"component\" option is deprecated. Please use the second parameter instead, e.g. \\n{ \\n\\tdefault: () => any \\n}',\n    )\n    slots.value.default = () => options.component\n  }\n\n  const modal = defineComponent({\n    setup(_, ctx) {\n      const mergedProps = computed(() => ({\n        ...props.value,\n        ...ctx.attrs,\n        open: open.value,\n        'onUpdate:open': (val: boolean) => (open.value = val),\n      }))\n      return () => h(Modal, mergedProps.value, { ...slots.value, ...ctx.slots })\n    },\n  })\n\n  const api = {\n    open: () => (open.value = true),\n    close: () => (open.value = false),\n    setProps(options: Partial<ModalProps> & Recordable) {\n      props.value = options\n      return this\n    },\n    patchProps(options: Partial<ModalProps> & Recordable) {\n      Object.assign(props.value, options)\n      return this\n    },\n    setSlots(_slots: ModalSlots) {\n      slots.value = _slots\n      return this\n    },\n    patchSlots(_slots: ModalSlots) {\n      Object.assign(slots.value, _slots)\n      return this\n    },\n    setContent<C extends new (...args: any) => any>(\n      Comp: C,\n      _props?: InstanceType<C>['$props'],\n      _slots?: InstanceType<C>['$slots'],\n      replace = true,\n    ) {\n      const defaultSlot = () =>\n        h(\n          Comp,\n          {\n            ..._props,\n            ref: (el: ComponentPublicInstance<{ modalSlots: ModalSlots }> | null) => {\n              if (el?.modalSlots) this.patchSlots(el?.modalSlots || {})\n            },\n          },\n          _slots,\n        )\n      if (replace) {\n        slots.value = { default: defaultSlot }\n      } else {\n        slots.value.default = defaultSlot\n      }\n      return this\n    },\n    // Compatibility code\n    setComponent(comp: VNode) {\n      console.warn('[Deprecated] \"setComponent\" is deprecated. Please use \"setContent\" instead.')\n      slots.value.default = () => comp\n      return this\n    },\n  }\n\n  return [modal, api] as const\n}\n"
  },
  {
    "path": "frontend/src/components/Modal/index.vue",
    "content": "<script lang=\"ts\">\nexport const IS_IN_MODAL = 'IS_IN_MODAL'\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, provide, ref, watch } from 'vue'\n\nimport { useBool } from '@/hooks'\nimport useI18n from '@/lang'\nimport { useAppStore } from '@/stores'\nimport { message } from '@/utils'\n\nexport interface Props {\n  title?: string\n  footer?: boolean\n  maxHeight?: string\n  maxWidth?: string\n  minWidth?: string\n  minHeight?: string\n  width?: string\n  height?: string\n  cancel?: boolean\n  submit?: boolean\n  cancelText?: string\n  submitText?: string\n  maskClosable?: boolean\n  class?: string\n  toolbar?: {\n    maximize?: boolean\n    minimize?: boolean\n    close?: boolean\n  }\n  onOk?: () => MaybePromise<boolean | void>\n  onCancel?: () => MaybePromise<boolean | void>\n  beforeClose?: (isOk: boolean) => MaybePromise<boolean | void>\n  afterClose?: (isOk: boolean) => void\n}\n\nexport interface Slots {\n  default?: () => any\n  title?: () => any\n  toolbar?: () => any\n  action?: () => any\n  cancel?: () => any\n  submit?: () => any\n}\n\nconst slots = defineSlots<Slots>()\n\nconst props = withDefaults(defineProps<Props>(), {\n  title: '',\n  footer: true,\n  maxHeight: '90',\n  maxWidth: '90',\n  minWidth: '60',\n  minHeight: '',\n  width: '',\n  height: '',\n  cancel: true,\n  submit: true,\n  cancelText: 'common.cancel',\n  submitText: 'common.save',\n  maskClosable: false,\n  class: undefined,\n  toolbar: () => ({\n    maximize: true,\n    minimize: true,\n  }),\n  onOk: undefined,\n  onCancel: undefined,\n  beforeClose: undefined,\n  afterClose: undefined,\n})\n\nconst open = defineModel<boolean>('open', { default: false })\n\nconst cancelLoading = ref(false)\nconst submitLoading = ref(false)\n\nconst modalZindex = ref()\nconst appStore = useAppStore()\nconst [isMaximize, toggleMaximize] = useBool(false)\n// const [isMinimize, toggleMinimize] = useBool(false)\n\nconst { t } = useI18n.global\n\nconst handleAction = async (isOk: boolean) => {\n  const loading = isOk ? submitLoading : cancelLoading\n  const action = isOk ? props.onOk : props.onCancel\n\n  loading.value = true\n  try {\n    if (!((await action?.()) ?? true) || !((await props.beforeClose?.(isOk)) ?? true)) {\n      return\n    }\n  } finally {\n    loading.value = false\n  }\n\n  open.value = false\n  props.afterClose?.(isOk)\n}\n\nconst handleSubmit = () => handleAction(true)\nconst handleCancel = () => handleAction(false)\n\nconst onMaskClick = () => props.maskClosable && handleCancel()\n\nconst contentStyle = computed(() => ({\n  maxHeight: props.maxHeight + '%',\n  maxWidth: props.maxWidth + '%',\n  minWidth: isMaximize.value ? '100%' : props.minWidth ? props.minWidth + '%' : '0',\n  minHeight: isMaximize.value ? '100%' : props.minHeight ? props.minHeight + '%' : '0',\n  width: props.width + '%',\n  height: props.height + '%',\n}))\n\nlet lastEscTime = 0\nlet closeMessage: () => void\n\nconst closeFn = () => {\n  if (isMaximize.value) {\n    toggleMaximize()\n    return\n  }\n  if (props.maskClosable) {\n    handleCancel()\n    return\n  }\n  const now = performance.now()\n  if (now - lastEscTime < 1000) {\n    handleCancel()\n    lastEscTime = 0\n    closeMessage?.()\n  } else {\n    const { destroy } = message.info('common.pressAgainToClose', 1_000)\n    closeMessage = destroy\n    lastEscTime = now\n  }\n}\n\nwatch(open, (v) => {\n  if (v) {\n    modalZindex.value = ++appStore.modalZIndexCounter\n    appStore.modalStack.push(closeFn)\n  } else {\n    closeMessage?.()\n    const idx = appStore.modalStack.findIndex((fn) => fn === closeFn)\n    if (idx !== -1) {\n      appStore.modalStack.splice(idx, 1)\n    }\n  }\n})\n\nprovide('cancel', handleCancel)\nprovide('submit', handleSubmit)\nprovide(IS_IN_MODAL, true)\n</script>\n\n<template>\n  <Teleport to=\"body\">\n    <Transition name=\"modal\" :duration=\"200\">\n      <div\n        v-if=\"open\"\n        :style=\"{ zIndex: modalZindex }\"\n        class=\"gui-modal-mask fixed inset-0 flex items-center justify-center backdrop-blur-sm\"\n        style=\"--wails-draggable: drag\"\n        @click.self=\"onMaskClick\"\n      >\n        <div\n          :style=\"contentStyle\"\n          :class=\"props.class\"\n          class=\"gui-modal-modal transition duration-200 flex flex-col rounded-8 shadow\"\n          style=\"--wails-draggable: false\"\n        >\n          <div\n            v-if=\"title || slots.title || slots.toolbar\"\n            class=\"flex items-center p-16\"\n            style=\"--wails-draggable: drag\"\n            @dblclick.self=\"toggleMaximize\"\n          >\n            <slot name=\"title\">\n              <div v-if=\"title\" class=\"font-bold\">{{ t(title) }}</div>\n            </slot>\n            <div class=\"ml-auto\" style=\"--wails-draggable: false\">\n              <slot name=\"toolbar\"></slot>\n              <!-- <Button v-if=\"toolbar.minimize\" @click=\"toggleMinimize\" icon=\"minimize\" type=\"text\" /> -->\n              <Button v-if=\"toolbar.maximize\" type=\"text\" @click=\"toggleMaximize\">\n                <Icon\n                  :class=\"{ maximize: isMaximize }\"\n                  icon=\"arrowDown\"\n                  class=\"maximize-normal origin-center duration-200\"\n                />\n              </Button>\n            </div>\n          </div>\n          <div class=\"flex-1 overflow-auto mx-16\">\n            <slot></slot>\n          </div>\n          <div v-if=\"footer\" class=\"flex items-center justify-end py-8 px-16 gap-8\">\n            <slot name=\"action\"></slot>\n            <slot name=\"cancel\">\n              <Button\n                v-if=\"cancel\"\n                :loading=\"cancelLoading\"\n                :type=\"maskClosable ? 'text' : 'normal'\"\n                @click=\"handleCancel\"\n              >\n                {{ t(cancelText) }}\n              </Button>\n            </slot>\n            <slot name=\"submit\">\n              <Button v-if=\"submit\" :loading=\"submitLoading\" type=\"primary\" @click=\"handleSubmit\">\n                {{ t(submitText) }}\n              </Button>\n            </slot>\n          </div>\n        </div>\n      </div>\n    </Transition>\n  </Teleport>\n</template>\n\n<style lang=\"less\" scoped>\n.modal-enter-active .gui-modal-modal,\n.modal-leave-active .gui-modal-modal {\n  transition:\n    transform 0.2s ease-in-out,\n    opacity 0.2s ease-in-out;\n}\n\n.modal-enter-from .gui-modal-modal,\n.modal-leave-to .gui-modal-modal {\n  opacity: 0;\n  transform: scale(0);\n}\n\n.gui-modal-mask {\n  background-color: var(--modal-mask-bg);\n\n  .gui-modal-modal {\n    background-color: var(--modal-bg);\n  }\n}\n.maximize-normal {\n  transform: rotate(-180deg);\n}\n.maximize {\n  transform: rotate(0deg);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/MultipleSelect/index.vue",
    "content": "<template>\n  <Select v-bind=\"$attrs\" multiple />\n</template>\n"
  },
  {
    "path": "frontend/src/components/Pagination/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue'\n\ninterface Props {\n  total: number\n  size?: 'default' | 'small' | 'large'\n  pageSize?: number\n}\n\nconst model = defineModel<number>('current', { default: 1 })\nconst props = withDefaults(defineProps<Props>(), { size: 'default', pageSize: 9 })\n\nconst pageNum = computed(() => Math.ceil(props.total / props.pageSize))\nconst pages = computed(() => {\n  const total = pageNum.value\n  const current = model.value\n  if (total <= 8) return range(1, total)\n  if (current <= 4) {\n    return [...range(1, 7), 'next', total] as const\n  } else if (current >= total - 3) {\n    return [1, 'prev', ...range(total - 6, total)] as const\n  } else {\n    return [1, 'prev', ...range(current - 2, current + 2), 'next', total] as const\n  }\n})\n\nconst range = (start: number, end: number) => {\n  return Array.from({ length: end - start + 1 }, (_, i) => start + i)\n}\nconst handlePrev = () => (model.value = Math.max(1, model.value - 1))\nconst handleNext = () => (model.value = Math.min(pageNum.value, model.value + 1))\nconst handleJump = (page: number | 'prev' | 'next') => {\n  if (typeof page === 'number') {\n    model.value = page\n    return\n  }\n  if (page === 'prev') {\n    model.value = Math.max(1, model.value - 5)\n  } else if (page === 'next') {\n    model.value = Math.min(pageNum.value, model.value + 5)\n  }\n}\n</script>\n\n<template>\n  <div>\n    <Button icon=\"arrowLeft\" type=\"text\" :size=\"size\" @click=\"handlePrev\" />\n    <Button v-if=\"pages.length === 0\" type=\"text\" :size=\"size\"> ... </Button>\n    <Button\n      v-for=\"item in pages\"\n      :key=\"item\"\n      :type=\"item === model ? 'primary' : 'text'\"\n      :size=\"size\"\n      class=\"min-w-32\"\n      @click=\"handleJump(item)\"\n    >\n      {{ item === 'prev' || item === 'next' ? '...' : item }}\n    </Button>\n    <Button icon=\"arrowRight\" type=\"text\" :size=\"size\" @click=\"handleNext\" />\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/components/Picker/index.vue",
    "content": "<script setup lang=\"ts\" generic=\"ValueType = any, PickerType extends 'single' | 'multi' = 'single'\">\nimport { ref, toRaw, type Ref } from 'vue'\n\nimport useI18n from '@/lang'\n\nexport type PickerItem<T> = {\n  label: string\n  value: T\n  description?: string\n  background?: string\n  onSelect?: (args: {\n    value: PickerItem<T>['value']\n    option: PickerItem<T>\n    options: PickerItem<T>[]\n    selected: PickerItem<T>['value'][]\n  }) => void\n}\n\ninterface Props<T, K> {\n  type: K\n  title: string\n  options?: PickerItem<T>[]\n  initialValue?: T[]\n}\n\nconst props = withDefaults(defineProps<Props<ValueType, PickerType>>(), {\n  options: () => [],\n  initialValue: () => [],\n})\n\nconst emit = defineEmits<{\n  confirm: [val: PickerType extends 'single' ? ValueType : ValueType[]]\n  cancel: []\n  finish: []\n}>()\n\nconst selected = ref(\n  new Set(\n    props.initialValue.filter((v) => props.options.find((o) => o.value === v)).map((v) => toRaw(v)),\n  ),\n) as Ref<Set<ValueType>>\n\nconst { t } = useI18n.global\n\nconst handleConfirm = () => {\n  const res: any = Array.from(selected.value).map((v) => toRaw(v))\n  if (props.type === 'single') {\n    emit('confirm', res[0])\n  } else {\n    emit('confirm', res)\n  }\n  emit('finish')\n}\n\nconst handleCancel = () => {\n  emit('cancel')\n  emit('finish')\n}\n\nconst isSelected = (option: ValueType) => selected.value.has(option)\n\nconst handleSelect = (option: PickerItem<ValueType>) => {\n  if (isSelected(option.value)) {\n    selected.value.delete(option.value)\n  } else {\n    if (props.type === 'single') selected.value.clear()\n    selected.value.add(option.value)\n    option.onSelect?.({\n      value: option.value,\n      option,\n      options: props.options,\n      selected: Array.from(selected.value).map((v) => toRaw(v)),\n    })\n  }\n}\n\nconst handleSelectAll = () => {\n  if (props.options.some((v) => !selected.value.has(v.value))) {\n    props.options.forEach((v) => selected.value.add(v.value))\n  } else {\n    selected.value.clear()\n  }\n}\n</script>\n\n<template>\n  <Transition name=\"slide-down\" appear>\n    <div class=\"gui-picker flex flex-col p-8 shadow rounded-8\">\n      <div class=\"font-bold px-4 py-8\">{{ t(title) }}</div>\n\n      <div class=\"flex-1 overflow-auto\">\n        <div\n          v-for=\"(o, i) in options\"\n          :key=\"i\"\n          :style=\"{ background: o.background }\"\n          class=\"item my-4 py-8 px-8 break-all\"\n          @click=\"handleSelect(o)\"\n        >\n          <div class=\"flex items-center justify-between leading-relaxed\">\n            <div class=\"font-bold\">{{ t(o.label) }}</div>\n            <Icon\n              v-show=\"isSelected(o.value)\"\n              :size=\"26\"\n              icon=\"selected\"\n              color=\"var(--primary-color)\"\n              class=\"shrink-0\"\n            />\n          </div>\n          <div class=\"text-12 leading-relaxed\" style=\"opacity: 0.7\">{{ o.description }}</div>\n        </div>\n      </div>\n\n      <div class=\"form-action gap-4\">\n        <Button v-if=\"type === 'multi'\" type=\"text\" size=\"small\" @click=\"handleSelectAll\">\n          {{ t('common.selectAll') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" class=\"mr-auto\">\n          {{ selected.size }} / {{ options.length }}\n        </Button>\n        <Button size=\"small\" @click=\"handleCancel\">{{ t('common.cancel') }}</Button>\n        <Button size=\"small\" type=\"primary\" @click=\"handleConfirm\">\n          {{ t('common.confirm') }}\n        </Button>\n      </div>\n    </div>\n  </Transition>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-picker {\n  min-width: 340px;\n  max-width: 60%;\n  background: var(--toast-bg);\n\n  .item {\n    &:nth-child(odd) {\n      background: var(--table-tr-odd-bg);\n    }\n    &:nth-child(even) {\n      background: var(--table-tr-even-bg);\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Progress/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\ninterface Props {\n  percent: number\n  status?: 'primary' | 'warning' | 'danger'\n  type?: 'circle' | 'line'\n  radius?: number\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  status: 'primary',\n  type: 'line',\n  radius: 100,\n})\n\nconst innerStyle = computed(() => ({\n  width: (props.percent > 100 ? 100 : props.percent || 0) + '%',\n}))\n\nconst circleStyle = computed(() => {\n  const color = { warning: '#FFC107', danger: '#F44336', primary: 'var(--progress-inner-bg)' }[\n    props.status\n  ]\n  const radius = props.radius * 2 + 'px'\n  const percent = Math.min(props.percent || 0, 100)\n  const mask = `radial-gradient(transparent ${props.radius * 0.6}px, #fff 0px)`\n  const bg = `conic-gradient(${color} 0%, ${color} ${percent}%, var(--progress-bg) ${percent}%, var(--progress-bg) 100%)`\n  return {\n    width: radius,\n    height: radius,\n    background: bg,\n    mask: mask,\n    '-webkit-mask': mask,\n  }\n})\n</script>\n\n<template>\n  <div v-if=\"type === 'line'\" class=\"gui-progress-line h-10 rounded-8 overflow-hidden\">\n    <div\n      :style=\"innerStyle\"\n      :class=\"props.status\"\n      class=\"inner h-full rounded-8 duration-200\"\n    ></div>\n  </div>\n  <div\n    v-if=\"type === 'circle'\"\n    :style=\"circleStyle\"\n    class=\"gui-progress-circle relative rounded-full\"\n  ></div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-progress-line {\n  background-color: var(--progress-bg);\n  .inner {\n    background-color: var(--progress-inner-bg);\n  }\n  .warning {\n    background-color: #ffc107;\n  }\n  .danger {\n    background-color: #f44336;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Prompt/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, unref } from 'vue'\n\nimport useI18n from '@/lang'\n\nimport { type Props as InputProps } from '@/components/Input/index.vue'\n\ninterface Props {\n  title: string\n  initialValue?: string | number\n  props: Omit<InputProps, 'modelValue'>\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  placeholder: '',\n  initialValue: '',\n})\n\nconst emits = defineEmits(['submit', 'cancel', 'finish'])\n\nconst type = typeof props.initialValue === 'string' ? 'text' : 'number'\nconst value = ref(props.initialValue)\n\nconst { t } = useI18n.global\n\nconst handleSubmit = (e: Event) => {\n  if (e.type === 'keydown' && props.props.type === 'code') return\n  emits('submit', unref(value))\n  emits('finish')\n}\n\nconst handleCancel = () => {\n  emits('cancel')\n  emits('finish')\n}\n</script>\n\n<template>\n  <Transition name=\"slide-down\" appear>\n    <div class=\"gui-confirm p-8 rounded-8 shadow max-w-[60%]\">\n      <div class=\"font-bold break-all px-4 py-8\">{{ t(title) }}</div>\n      <Input\n        v-model=\"value\"\n        v-bind=\"props.props\"\n        :type=\"props.props.type || type\"\n        autofocus\n        clearable\n        size=\"small\"\n        class=\"w-full\"\n        @keydown.enter=\"handleSubmit\"\n      />\n      <div class=\"form-action gap-4\">\n        <Button size=\"small\" @click=\"handleCancel\">{{ t('common.cancel') }}</Button>\n        <Button size=\"small\" type=\"primary\" @click=\"handleSubmit\">\n          {{ t('common.confirm') }}\n        </Button>\n      </div>\n    </div>\n  </Transition>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-confirm {\n  min-width: 340px;\n  background: var(--toast-bg);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Radio/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n'\n\ninterface Props {\n  options?: { label: string; value: string | number | boolean }[]\n  size?: 'default' | 'small'\n}\n\nconst model = defineModel<string | number | boolean>()\n\nwithDefaults(defineProps<Props>(), {\n  options: () => [],\n  size: 'default',\n})\n\nconst emits = defineEmits(['change'])\n\nconst { t } = useI18n()\n\nconst handleSelect = (val: string | number | boolean) => {\n  const oldValue = model.value\n  if (oldValue === val) {\n    return\n  }\n  model.value = val\n  emits('change', val, oldValue)\n}\n</script>\n\n<template>\n  <div :class=\"[size]\" class=\"gui-radio inline-flex rounded-full text-12 overflow-hidden\">\n    <div\n      v-for=\"o in options\"\n      :key=\"o.value.toString()\"\n      v-tips.slow=\"o.label\"\n      :class=\"{ active: o.value === model }\"\n      class=\"gui-radio-button cursor-pointer px-12 py-6 duration-200 line-clamp-1 break-all\"\n      @click=\"handleSelect(o.value)\"\n    >\n      {{ t(o.label) }}\n    </div>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-radio {\n  border: 1px solid var(--primary-color);\n  &-button {\n    color: var(--radio-normal-color);\n    background-color: var(--radio-normal-bg);\n    border-left: 1px solid var(--primary-color);\n    &:nth-child(1) {\n      border-left: none;\n    }\n    &:hover {\n      color: var(--radio-normal-hover-color);\n    }\n  }\n  .active {\n    color: var(--radio-primary-color);\n    background-color: var(--radio-primary-bg);\n    &:hover {\n      background-color: var(--radio-primary-hover-bg);\n    }\n    &:active {\n      background-color: var(--radio-primary-active-bg);\n    }\n  }\n}\n\n.small {\n  .gui-radio-button {\n    font-size: 10px;\n    padding: 4px 8px;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Select/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { deepClone } from '@/utils'\n\ninterface Props {\n  modelValue?: string | string[]\n  options?: { label: string; value: string }[]\n  multiple?: boolean\n  border?: boolean\n  size?: 'default' | 'small'\n  placeholder?: string\n  autoSize?: boolean\n  clearable?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  modelValue: undefined,\n  options: () => [],\n  multiple: false,\n  border: true,\n  size: 'default',\n  placeholder: '',\n  autoSize: false,\n  clearable: false,\n})\n\nconst emit = defineEmits(['change', 'update:modelValue'])\n\nconst model = ref(props.multiple ? deepClone(props.modelValue || []) : props.modelValue)\n\nconst { t } = useI18n()\n\nconst innerClearable = computed(\n  () => props.clearable && (props.multiple ? (model.value as string[]).length !== 0 : model.value),\n)\n\nconst optionsValueLabelMapping = computed(() =>\n  props.options.reduce((p, c) => {\n    p[c.value] = c.label ?? c.value\n    return p\n  }, {} as Recordable),\n)\n\nconst displayLabel = computed(() => {\n  if (props.multiple) {\n    const selected = model.value as string[]\n    if (selected.length === 0) {\n      return props.placeholder ?? 'common.none'\n    }\n    return selected.map((item) => t(optionsValueLabelMapping.value[item] ?? item)).join('、')\n  }\n  const label = props.options.find((v) => v.value === model.value)?.label ?? (model.value as string)\n  return (label || props.placeholder) ?? 'common.none'\n})\n\nlet internalUpdate = false\n\nwatch(\n  () => props.modelValue,\n  (val) => {\n    if (!internalUpdate) {\n      model.value = val\n    }\n    internalUpdate = false\n  },\n  { deep: true },\n)\n\nconst isSelected = (val: string) => {\n  if (props.multiple) {\n    return (model.value as string[]).includes(val)\n  }\n  return model.value === val\n}\n\nconst handleSelect = (value: string) => {\n  const oldModel = JSON.stringify(model.value)\n  if (props.multiple) {\n    if (!Array.isArray(model.value)) {\n      model.value = []\n    }\n    const idx = model.value?.indexOf(value) ?? -1\n    if (idx === -1) {\n      ;(model.value as string[]).push(value)\n    } else {\n      ;(model.value as string[]).splice(idx, 1)\n    }\n    if (oldModel !== JSON.stringify(model.value)) {\n      emit('update:modelValue', model.value)\n      emit('change', model.value)\n    }\n  } else if (value !== model.value) {\n    model.value = value\n    emit('update:modelValue', model.value)\n    emit('change', model.value)\n  }\n  internalUpdate = true\n}\n\nconst handleClear = () => {\n  if (props.multiple) {\n    model.value = []\n    emit('update:modelValue', [])\n    emit('change', [])\n  } else {\n    model.value = ''\n    emit('update:modelValue', '')\n    emit('change', '')\n  }\n  internalUpdate = true\n}\n</script>\n\n<template>\n  <Dropdown :trigger=\"['click']\">\n    <template #default=\"{ toggle, close }\">\n      <div\n        :class=\"{\n          border,\n          [size]: true,\n          'auto-size': autoSize,\n          'min-h-28': size === 'small',\n          'min-h-30': size === 'default',\n        }\"\n        class=\"gui-select cursor-pointer inline-flex items-center min-w-128 rounded-4 px-8\"\n      >\n        <span class=\"line-clamp-1 break-all\">\n          {{ t(displayLabel) }}\n        </span>\n        <Button\n          :icon=\"innerClearable ? 'close' : 'arrowDown'\"\n          type=\"text\"\n          size=\"small\"\n          class=\"ml-auto\"\n          style=\"margin-right: -6px\"\n          @click.stop=\"\n            () => {\n              if (innerClearable) {\n                handleClear()\n                close()\n              } else {\n                toggle()\n              }\n            }\n          \"\n        />\n      </div>\n    </template>\n\n    <template #overlay=\"{ close }\">\n      <div class=\"flex flex-col gap-4 min-w-64 p-4\">\n        <slot v-if=\"options.length === 0\" name=\"empty\">\n          <Empty :icon-size=\"42\" />\n        </slot>\n        <Button\n          v-for=\"o in options\"\n          :key=\"o.value\"\n          type=\"text\"\n          @click=\"\n            () => {\n              handleSelect(o.value)\n              !props.multiple && close()\n            }\n          \"\n        >\n          <div class=\"realtive w-full\">\n            <div v-if=\"isSelected(o.value)\" class=\"absolute left-8\">\n              <Icon icon=\"selected\" :size=\"18\" />\n            </div>\n            <div class=\"\">\n              {{ t(o.label) }}\n            </div>\n          </div>\n        </Button>\n      </div>\n    </template>\n  </Dropdown>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-select {\n  background: var(--select-bg);\n}\n\n.auto-size {\n  width: 100%;\n}\n\n.border {\n  border: 1px solid var(--primary-color);\n}\n\n.small {\n  font-size: 12px;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Switch/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { nextTick } from 'vue'\n\nimport i18n from '@/lang'\n\ninterface Props {\n  size?: 'default' | 'small'\n  border?: 'default' | 'square'\n  label?: string\n  disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  size: 'default',\n  border: 'default',\n  label: '',\n  disabled: false,\n})\n\nconst model = defineModel<boolean>({ default: false })\n\nconst emits = defineEmits<{\n  (e: 'change', val: boolean): void\n}>()\n\nconst { t } = i18n.global\n\nconst toggle = () => {\n  if (props.disabled) return\n  model.value = !model.value\n  nextTick(() => emits('change', model.value))\n}\n</script>\n\n<template>\n  <div\n    v-tips.slow=\"label\"\n    :class=\"[\n      size,\n      border,\n      model ? 'on' : 'off',\n      border === 'square' ? 'rounded-4' : 'rounded-full',\n      { 'cursor-not-allowed': disabled },\n    ]\"\n    class=\"gui-switch relative cursor-pointer h-24 inline-flex items-center text-12 duration-200\"\n    @click=\"toggle\"\n  >\n    <div\n      :class=\"[border === 'square' ? 'rounded-4' : 'rounded-full']\"\n      class=\"dot absolute h-18 w-18 duration-200\"\n    ></div>\n\n    <div v-if=\"$slots.default || label\" class=\"slot line-clamp-1 break-all\">\n      <span v-if=\"label\">{{ t(label) }}</span>\n      <slot v-if=\"$slots.default\"></slot>\n    </div>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-switch {\n  min-width: 50px;\n  // .slot {\n  //   transition: margin 0.2s;\n  // }\n}\n\n.small {\n  height: 20px;\n  .dot {\n    width: 12px;\n    height: 12px;\n  }\n}\n\n.square {\n  .dot {\n    width: 4px;\n  }\n}\n\n.on {\n  color: #fff;\n  background-color: var(--switch-on-bg);\n  &:hover {\n    background-color: var(--switch-on-hover-bg);\n  }\n  .dot {\n    left: calc(100% - 22px);\n    background-color: var(--switch-on-dot-bg);\n  }\n\n  .slot {\n    margin-right: 26px;\n    margin-left: 10px;\n  }\n\n  &.small {\n    .dot {\n      left: calc(100% - 16px);\n    }\n    .slot {\n      margin-right: 20px;\n      margin-left: 8px;\n    }\n  }\n\n  &.square {\n    .dot {\n      left: calc(100% - 8px);\n    }\n    .slot {\n      margin-right: 12px;\n      margin-left: 8px;\n    }\n  }\n}\n\n.off {\n  color: var(--card-color);\n  background-color: var(--switch-off-bg);\n  &:hover {\n    background-color: var(--switch-off-hover-bg);\n  }\n  .dot {\n    left: 4px;\n    background-color: var(--switch-off-dot-bg);\n  }\n\n  .slot {\n    margin-left: 26px;\n    margin-right: 10px;\n  }\n\n  &.small {\n    .dot {\n      left: 4px;\n    }\n    .slot {\n      margin-left: 20px;\n      margin-right: 8px;\n    }\n  }\n\n  &.square {\n    .slot {\n      margin-left: 12px;\n      margin-right: 8px;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Table/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, computed, isVNode, h } from 'vue'\n\nimport vMenu from '@/directives/menu'\nimport useI18n from '@/lang'\nimport { getValue } from '@/utils'\n\nimport type { Menu } from '@/types/app'\n\nexport type Column = {\n  title: string\n  key: string\n  align?: 'center' | 'left' | 'right'\n  hidden?: boolean\n  minWidth?: string\n  sort?: (a: Record<string, any>, b: Record<string, any>) => number\n  customRender?: (v: { value: any; record: Record<string, any> }) => any\n}\n\ninterface Props {\n  menu?: Menu[]\n  columns: Column[]\n  dataSource: Record<string, any>[]\n  sort?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  menu: () => [],\n  sort: undefined,\n})\n\nconst sortField = ref(props.sort)\nconst sortReverse = ref(true)\nconst sortFunc = computed(\n  () => props.columns.find((column) => column.key === sortField.value)?.sort,\n)\n\nconst { t } = useI18n.global\n\nconst handleChangeSortField = (field: string) => {\n  if (sortField.value === field) {\n    if (sortReverse.value) {\n      sortReverse.value = false\n      return\n    }\n    sortField.value = ''\n    sortReverse.value = true\n    return\n  }\n  sortField.value = field\n  sortReverse.value = true\n}\n\nconst tableData = computed(() => {\n  if (!sortField.value || !sortFunc.value) return props.dataSource\n  const sorted = props.dataSource.slice().sort(sortFunc.value)\n  if (sortReverse.value) sorted.reverse()\n  return sorted\n})\n\nconst tableColumns = computed(() => {\n  return props.columns.filter((column) => !column.hidden)\n})\n\nconst renderCell = (column: Column, record: Recordable) => {\n  const value = getValue(record, column.key)\n  let result = column.customRender?.({ value, record }) ?? value ?? '-'\n  if (!isVNode(result)) {\n    result = h('div', String(result))\n  }\n  return result\n}\n</script>\n\n<template>\n  <div class=\"gui-table overflow-auto\">\n    <table class=\"w-full text-12 border-collapse\">\n      <thead>\n        <tr class=\"sticky top-0 shadow\">\n          <th\n            v-for=\"column in tableColumns\"\n            :key=\"column.key\"\n            class=\"px-4 py-8 whitespace-nowrap cursor-pointer\"\n          >\n            <div\n              :style=\"{\n                justifyContent: { left: 'flext-start', center: 'center', right: 'flex-end' }[\n                  column.align || 'left'\n                ],\n                minWidth: column.minWidth || 'auto',\n              }\"\n              class=\"flex items-center\"\n              @click=\"handleChangeSortField(column.key)\"\n            >\n              {{ t(column.title) }}\n              <div v-if=\"sortField === column.key && sortFunc\">\n                <span class=\"px-4\"> {{ sortReverse ? '↑' : '↓' }} </span>\n              </div>\n            </div>\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr\n          v-for=\"record in tableData\"\n          :key=\"record.id\"\n          v-menu=\"menu.map((v) => ({ ...v, handler: () => v.handler?.(record) }))\"\n          class=\"transition duration-200\"\n        >\n          <td\n            v-for=\"column in tableColumns\"\n            :key=\"column.key\"\n            :style=\"{ textAlign: column.align || 'left' }\"\n            class=\"select-text whitespace-nowrap p-8\"\n          >\n            <slot :name=\"column.key\" :=\"{ column, record }\">\n              <component :is=\"renderCell(column, record)\" />\n            </slot>\n          </td>\n        </tr>\n      </tbody>\n    </table>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\ntable {\n  thead {\n    tr {\n      background: var(--table-tr-odd-bg);\n    }\n  }\n  tbody {\n    tr {\n      &:nth-child(odd) {\n        background: var(--table-tr-odd-bg);\n        &:hover {\n          background: var(--table-tr-odd-hover-bg);\n        }\n      }\n      &:nth-child(even) {\n        background: var(--table-tr-even-bg);\n        &:hover {\n          background: var(--table-tr-even-hover-bg);\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Tabs/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, useSlots, type Component } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\ntype TabItemType = {\n  key: string\n  tab: string\n  component?: Component\n}\n\ninterface Props {\n  activeKey: string\n  items: readonly TabItemType[]\n  tabPosition?: 'left' | 'top'\n  tabWidth?: string\n  contentWidth?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  tabPosition: 'left',\n  tabWidth: '20%',\n  contentWidth: '80%',\n})\n\nconst emits = defineEmits(['update:activeKey'])\n\nconst { t } = useI18n()\nconst slots = useSlots()\n\nconst isTop = computed(() => props.tabPosition === 'top')\n\nconst handleChange = (key: string) => emits('update:activeKey', key)\n\nconst isActive = ({ key }: TabItemType) => key === props.activeKey\n\n// NOTE:\n// - component tabs are cached via KeepAlive\n// - slot tabs are rendered as functional components and NOT cached\nconst currentComponent = computed(() => {\n  const comp = props.items.find((i) => i.key === props.activeKey)?.component\n  return comp ?? slots[props.activeKey]\n})\n</script>\n\n<template>\n  <div :class=\"{ 'flex-col': isTop }\" class=\"gui-tabs flex\">\n    <div\n      :class=\"{ 'justify-center mb-8': isTop, 'flex-col': !isTop }\"\n      :style=\"{ width: isTop ? 'auto' : tabWidth }\"\n      class=\"gui-tabs-tab flex\"\n    >\n      <Button\n        v-for=\"tab in items\"\n        :key=\"tab.key\"\n        :type=\"isActive(tab) ? 'link' : 'text'\"\n        @click=\"handleChange(tab.key)\"\n      >\n        {{ t(tab.tab) }}\n      </Button>\n      <slot name=\"extra\"></slot>\n    </div>\n\n    <div class=\"flex flex-col overflow-y-auto\" :style=\"{ width: isTop ? 'auto' : contentWidth }\">\n      <KeepAlive>\n        <component :is=\"currentComponent\" />\n      </KeepAlive>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/components/Tag/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\ninterface Props {\n  color?: 'cyan' | 'green' | 'red' | 'default' | 'primary' | 'orange' | 'gold' | 'blue' | 'purple'\n  size?: 'small' | 'default'\n  closeable?: boolean\n  bordered?: boolean\n}\n\nwithDefaults(defineProps<Props>(), {\n  color: 'default',\n  closable: false,\n  size: 'default',\n  bordered: true,\n})\n\nconst emit = defineEmits(['close'])\n\nconst show = ref(true)\nconst handleClose = () => {\n  emit('close')\n  show.value = false\n}\n</script>\n\n<template>\n  <div\n    v-if=\"show\"\n    :class=\"['color-' + color, 'size-' + size, { 'border-0': !bordered }]\"\n    class=\"gui-tag px-8 mx-4 rounded-6 inline-block text-12 whitespace-nowrap inline-flex items-center\"\n  >\n    <slot></slot>\n    <Icon\n      v-if=\"closeable\"\n      :size=\"size === 'small' ? 12 : 14\"\n      icon=\"close\"\n      class=\"ml-2\"\n      @click=\"handleClose\"\n    />\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.color-cyan {\n  color: #22a3a7;\n  background-color: #e6fffb;\n  border: 1px solid #22a3a7;\n}\n.color-green {\n  color: #389e0d;\n  background-color: #f6ffed;\n  border: 1px solid #389e0d;\n}\n.color-red {\n  color: #d52e3b;\n  background-color: #fff1f0;\n  border: 1px solid #d52e3b;\n}\n.color-default {\n  color: #3d3d3d;\n  background-color: #ffffff;\n  border: 1px solid #898989;\n}\n.color-primary {\n  color: var(--btn-primary-color);\n  background-color: var(--primary-color);\n  border: 1px solid var(--secondary-color);\n}\n.color-orange {\n  color: #d46b08;\n  background-color: #fff7e6;\n  border: 1px solid #d46b08;\n}\n.color-gold {\n  color: #d48806;\n  background-color: #fffbe6;\n  border: 1px solid #d48806;\n}\n.color-blue {\n  color: #0958d9;\n  background-color: #e6f4ff;\n  border: 1px solid #0958d9;\n}\n.color-purple {\n  color: #531dab;\n  background-color: #f9f0ff;\n  border: 1px solid #531dab;\n}\n\n.size-small {\n  padding: 0 4px;\n  margin: 0 2px;\n  font-size: 10px;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Tips/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, watch, nextTick, useTemplateRef } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\ninterface Props {\n  position: { x: number; y: number }\n  message?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  message: '',\n})\n\nconst model = defineModel<boolean>()\n\nconst domRef = useTemplateRef('domRef')\nconst fixedPosition = ref({ x: 0, y: 0 })\n\nconst { t } = useI18n()\n\nwatch(\n  () => props.position,\n  ({ x, y }) => {\n    if (!fixedPosition.value.x && !fixedPosition.value.y) {\n      fixedPosition.value = { x, y }\n    }\n    nextTick(() => {\n      if (domRef.value) {\n        x = x - domRef.value.offsetWidth / 2\n        y -= domRef.value.offsetHeight * 2\n        fixedPosition.value = { x, y }\n      }\n    })\n  },\n)\n</script>\n\n<template>\n  <div\n    v-show=\"model\"\n    ref=\"domRef\"\n    :style=\"{ left: fixedPosition.x + 'px', top: fixedPosition.y + 'px' }\"\n    class=\"gui-tips fixed z-9999 duration-100 pointer-events-none shadow whitespace-pre-wrap text-center text-12 p-4 rounded-8 min-w-64 backdrop-blur-sm\"\n  >\n    {{ t(message) }}\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-tips {\n  background: var(--menu-bg);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/TrafficChart/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref, watch, onActivated, useTemplateRef } from 'vue'\n\nimport { formatBytes } from '@/utils'\n\ninterface Props {\n  height?: number\n  padding?: number\n  legend?: string[]\n  series: number[][]\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  height: 214,\n  padding: 50,\n  legend: () => ['upload', 'download'],\n})\n\nconst MAX_HISTORY = 60\nconst svgRef = useTemplateRef<SVGAElement>('svgRef')\nconst width = ref(200)\nconst points = ref<string[]>([])\nconst showLines = ref([true, true])\nconst fillColors = ['#8851e350', '#2e9ae550']\n\nconst strokeColors = computed(() => {\n  const upload = showLines.value[0] ? '#8851e3' : 'gray'\n  const download = showLines.value[1] ? '#2e9ae5' : 'gray'\n  return [upload, download]\n})\n\nconst maxValue = computed(() => {\n  const maxUpload = Math.max(...props.series[0]!, props.height)\n  const maxDownload = Math.max(...props.series[1]!, props.height)\n  if (showLines.value[0] && showLines.value[1]) return Math.max(maxUpload, maxDownload)\n  if (showLines.value[0]) return maxUpload\n  if (showLines.value[1]) return maxDownload\n  return props.height\n})\n\nconst updateSvgWidth = () => {\n  if (svgRef.value) {\n    width.value = svgRef.value.clientWidth\n    updateChart()\n  }\n}\n\nconst updateChart = () => {\n  const { padding } = props\n  let { height } = props\n  const paddingY = height / 8\n  height -= paddingY\n  points.value = props.series.map((s, index) => {\n    if (!showLines.value[index]) return ''\n    const newS = [...s]\n    if (newS.length < MAX_HISTORY) {\n      newS.unshift(...Array.from({ length: MAX_HISTORY - s.length }, () => 0))\n    }\n    const spacing = (width.value - padding) / newS.length\n    const point = newS.reduce((p, c, i) => {\n      const x = Math.floor(i * spacing) + padding\n      const y = Math.floor(height - (c / maxValue.value) * height) + paddingY - 6\n      return i === 0 ? x + ',' + y : p + ',' + x + ',' + y\n    }, '')\n    const startPos = padding + ',' + (props.height - 6)\n    const endPos = Math.floor((MAX_HISTORY - 1) * spacing + padding) + ',' + (props.height - 6)\n    return startPos + ',' + point + ',' + endPos\n  })\n}\n\nconst toggleUpload = () => {\n  showLines.value[0] = !showLines.value[0]\n  updateChart()\n}\n\nconst toggleDownload = () => {\n  showLines.value[1] = !showLines.value[1]\n  updateChart()\n}\n\nonMounted(() => {\n  updateSvgWidth()\n  window.addEventListener('resize', updateSvgWidth)\n})\n\nonUnmounted(() => {\n  window.removeEventListener('resize', updateSvgWidth)\n})\n\nonActivated(updateSvgWidth)\n\nwatch(() => props.series, updateChart, { deep: true })\n</script>\n\n<template>\n  <div class=\"gui-traffic-chart rounded-8\">\n    <svg ref=\"svgRef\" :height=\"height + 'px'\" width=\"100%\" xmlns=\"http://www.w3.org/2000/svg\">\n      <text\n        v-for=\"i in 8\"\n        :key=\"i\"\n        :y=\"i * (height / 8) - 4\"\n        style=\"font-size: 8px\"\n        x=\"4\"\n        fill=\"var(--primary-color)\"\n      >\n        {{ formatBytes(maxValue - (i - 1) * (maxValue / 7)) }}\n      </text>\n\n      <line\n        v-for=\"i in 8\"\n        :key=\"i\"\n        :y1=\"i * (height / 8) - 7\"\n        :x2=\"width - 2\"\n        :y2=\"i * (height / 8) - 7\"\n        :x1=\"padding\"\n        stroke-dasharray=\"1 4\"\n        stroke=\"var(--color)\"\n      />\n\n      <template v-for=\"(point, index) in points\">\n        <polyline\n          v-if=\"showLines[index]\"\n          :key=\"index\"\n          :points=\"point\"\n          :stroke=\"strokeColors[index]\"\n          :fill=\"fillColors[index]\"\n          stroke-width=\"2\"\n        />\n      </template>\n\n      <circle\n        :cx=\"width / 2 - 40\"\n        :fill=\"strokeColors[0]\"\n        r=\"3\"\n        cy=\"10\"\n        class=\"text-10 cursor-pointer\"\n        @click=\"toggleUpload\"\n      />\n      <circle\n        :cx=\"width / 2 + 20\"\n        :fill=\"strokeColors[1]\"\n        r=\"3\"\n        cy=\"10\"\n        class=\"text-10 cursor-pointer\"\n        @click=\"toggleDownload\"\n      />\n      <text\n        :x=\"width / 2 - 34\"\n        :fill=\"strokeColors[0]\"\n        y=\"14\"\n        class=\"text-10 cursor-pointer\"\n        @click=\"toggleUpload\"\n      >\n        {{ legend[0] }}\n      </text>\n      <text\n        :x=\"width / 2 + 28\"\n        :fill=\"strokeColors[1]\"\n        y=\"14\"\n        class=\"text-10 cursor-pointer\"\n        @click=\"toggleDownload\"\n      >\n        {{ legend[1] }}\n      </text>\n    </svg>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.gui-traffic-chart {\n  background: var(--card-bg);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/_common/AboutView.vue",
    "content": "<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n'\n\nimport logo from '@/assets/logo'\nimport { RestartApp, BrowserOpenURL } from '@/bridge'\nimport { useAppStore, useEnvStore } from '@/stores'\nimport { APP_TITLE, APP_VERSION, PROJECT_URL, TG_GROUP, TG_CHANNEL, message } from '@/utils'\n\nconst { t } = useI18n()\nconst envStore = useEnvStore()\nconst appStore = useAppStore()\n\nconst handleRestartApp = async () => {\n  try {\n    await RestartApp()\n  } catch (error: any) {\n    message.error(error)\n  }\n}\n\nappStore.checkForUpdates()\n</script>\n\n<template>\n  <div class=\"flex flex-col items-center pt-36\">\n    <img :src=\"logo\" class=\"w-128\" draggable=\"false\" />\n    <div class=\"py-8 font-bold\">{{ APP_TITLE }}</div>\n    <div class=\"flex items-center pb-8 my-4\">\n      <Button\n        v-if=\"appStore.restartable\"\n        icon=\"restartApp\"\n        size=\"small\"\n        type=\"primary\"\n        @click=\"handleRestartApp\"\n      >\n        {{ t('about.restart') }}\n      </Button>\n      <template v-else>\n        <Button\n          :loading=\"appStore.checkForUpdatesLoading\"\n          type=\"link\"\n          size=\"small\"\n          @click=\"appStore.checkForUpdates(true)\"\n        >\n          Bridge: {{ envStore.env.appVersion }} - UI: {{ APP_VERSION }}\n        </Button>\n        <Button\n          v-if=\"appStore.updatable\"\n          :loading=\"appStore.downloading\"\n          size=\"small\"\n          @click=\"appStore.downloadApp\"\n        >\n          {{ t('about.new') }}: {{ appStore.remoteVersion }}\n        </Button>\n      </template>\n    </div>\n    <div\n      class=\"text-12 underline flex items-center cursor-pointer\"\n      @click=\"BrowserOpenURL(PROJECT_URL)\"\n    >\n      <Icon icon=\"github\" />GitHub\n    </div>\n    <div\n      class=\"text-12 underline flex items-center cursor-pointer\"\n      @click=\"BrowserOpenURL(TG_GROUP)\"\n    >\n      <Icon icon=\"telegram\" />Telegram Group\n    </div>\n    <div\n      class=\"text-12 underline flex items-center cursor-pointer\"\n      @click=\"BrowserOpenURL(TG_CHANNEL)\"\n    >\n      <Icon icon=\"telegram\" />Telegram Channel\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/components/_common/CommandView.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, computed, onMounted, onUnmounted, nextTick, watch, useTemplateRef } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useAppSettingsStore, useAppStore, usePluginsStore } from '@/stores'\nimport { debounce, message } from '@/utils'\nimport { getCommands } from '@/utils/command'\n\nimport Input from '@/components/Input/index.vue'\n\nconst loading = ref(false)\nconst showCommandPanel = ref(false)\nconst userInput = ref('')\nconst selected = ref(0)\nconst inputRef = useTemplateRef<typeof Input>('inputRef')\nconst commands = ref(getCommands())\nconst commandsRefMap: Record<string, HTMLElement> = {}\n\nconst hitCommand = computed(() =>\n  userInput.value\n    ? commands.value.filter(\n        (v) =>\n          v.cmd.toLocaleLowerCase().includes(userInput.value) ||\n          v.label.toLocaleLowerCase().includes(userInput.value),\n      )\n    : commands.value,\n)\n\nconst { t } = useI18n()\nconst appStore = useAppStore()\nconst appSettings = useAppSettingsStore()\nconst pluginsStore = usePluginsStore()\n\nconst handleExecCommand = async (index: number) => {\n  loading.value = true\n  try {\n    await hitCommand.value[index]?.handler?.()\n    userInput.value = ''\n    showCommandPanel.value = false\n  } catch (error: any) {\n    message.error(error.message || error)\n  }\n  loading.value = false\n  nextTick(inputRef.value!.focus)\n}\n\nconst onKeydown = async (ev: KeyboardEvent) => {\n  if (((ev.ctrlKey && ev.shiftKey) || ev.metaKey) && ev.code === 'KeyP') {\n    ev.preventDefault()\n    showCommandPanel.value = true\n    nextTick(inputRef.value!.focus)\n    return\n  }\n\n  if (!showCommandPanel.value || loading.value) return\n\n  if (ev.code === 'Escape') {\n    if (userInput.value) {\n      userInput.value = ''\n      return\n    }\n    showCommandPanel.value = false\n    return\n  }\n  if (ev.code === 'ArrowUp') {\n    selected.value = selected.value - 1 < 0 ? 0 : selected.value - 1\n    commandsRefMap[hitCommand.value[selected.value]!.label]?.scrollIntoView({ block: 'nearest' })\n    return\n  }\n  if (ev.code === 'ArrowDown') {\n    selected.value =\n      selected.value + 1 >= hitCommand.value.length\n        ? hitCommand.value.length - 1\n        : selected.value + 1\n    commandsRefMap[hitCommand.value[selected.value]!.label]?.scrollIntoView({ block: 'nearest' })\n    return\n  }\n  if (ev.code === 'Enter') {\n    if (hitCommand.value.length) {\n      await handleExecCommand(selected.value)\n    } else {\n      nextTick(inputRef.value!.focus)\n    }\n  }\n}\n\nwatch(hitCommand, () => (selected.value = 0))\n\nconst updateCommands = debounce(() => {\n  commands.value = getCommands()\n}, 200)\n\nwatch([() => appSettings.app.lang, pluginsStore.plugins, () => appStore.locales], updateCommands)\n\nonMounted(() => window.addEventListener('keydown', onKeydown))\nonUnmounted(() => window.removeEventListener('keydown', onKeydown))\n</script>\n\n<template>\n  <div\n    v-show=\"showCommandPanel\"\n    class=\"fixed z-9999 left-1/2 -translate-x-1/2 shadow rounded-4 min-w-[50%]\"\n    style=\"top: 40px; background: var(--modal-bg)\"\n  >\n    <div class=\"p-6 shadow\">\n      <Input\n        ref=\"inputRef\"\n        v-model=\"userInput\"\n        :disabled=\"loading\"\n        autofocus\n        clearable\n        class=\"w-full\"\n      >\n        <template #prefix>\n          <Icon icon=\"arrowRight\" />\n        </template>\n        <template #suffix>\n          <Icon v-show=\"loading\" icon=\"loading\" class=\"rotation\" />\n        </template>\n      </Input>\n    </div>\n    <div class=\"overflow-y-auto p-8\" style=\"max-height: calc(100vh - 130px)\">\n      <div\n        v-for=\"(c, index) in hitCommand\"\n        :key=\"c.label\"\n        :ref=\"(el: any) => (commandsRefMap[c.label] = el)\"\n      >\n        <Card\n          :title=\"c.label\"\n          :selected=\"index === selected\"\n          class=\"mt-4\"\n          style=\"font-size: 12px\"\n          @click=\"handleExecCommand(index)\"\n        >\n          <div>{{ c.desc }}</div>\n          <div>{{ c.cmd }}</div>\n        </Card>\n      </div>\n      <div v-show=\"hitCommand.length === 0\" class=\"p-4 text-12\">\n        {{ t('commands.noMatching') }}\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/components/_common/NavigationBar.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport rawRoutes from '@/router/routes'\nimport { useAppSettingsStore } from '@/stores'\n\nconst { t } = useI18n()\nconst appSettings = useAppSettingsStore()\n\nconst routes = computed(() =>\n  rawRoutes.filter(\n    (r) =>\n      r.meta?.hidden === false ||\n      (!r.meta?.hidden && appSettings.app.pages.includes(r.name! as string)),\n  ),\n)\n</script>\n\n<template>\n  <div class=\"flex items-center justify-center\">\n    <div v-for=\"r in routes\" :key=\"r.path\">\n      <RouterLink v-slot=\"{ navigate, isActive }\" :to=\"r.path\" custom>\n        <Button :type=\"isActive ? 'link' : 'text'\" :icon=\"r.meta && r.meta.icon\" @click=\"navigate\">\n          {{ (r.meta && t(r.meta.name)) || r.name }}\n        </Button>\n      </RouterLink>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/components/_common/SplashView.vue",
    "content": "<script setup lang=\"ts\">\nimport logo from '@/assets/logo'\nimport { APP_TITLE, APP_VERSION } from '@/utils/env'\n</script>\n\n<template>\n  <div class=\"h-full flex flex-col items-center justify-center\" style=\"--wails-draggable: drag\">\n    <img :src=\"logo\" class=\"w-128\" draggable=\"false\" />\n    <div class=\"text-24 font-bold mt-16 mb-24\">{{ APP_TITLE }}</div>\n    <div class=\"py-16\">{{ APP_VERSION }}</div>\n    <div class=\"rotation\">\n      <slot></slot>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/components/_common/TitleBar.vue",
    "content": "<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, ref } from 'vue'\n\nimport logo from '@/assets/logo'\nimport {\n  WindowSetAlwaysOnTop,\n  WindowHide,\n  WindowMinimise,\n  WindowSetSize,\n  WindowToggleMaximise,\n  WindowIsMaximised,\n  RestartApp,\n} from '@/bridge'\nimport { useAppSettingsStore, useKernelApiStore, useEnvStore, useAppStore } from '@/stores'\nimport { APP_TITLE, APP_VERSION, debounce, exitApp, reloadApp } from '@/utils'\n\nimport type { Menu } from '@/types/app'\n\nconst isPinned = ref(false)\nconst isMaximised = ref(false)\n\nconst appSettingsStore = useAppSettingsStore()\nconst kernelApiStore = useKernelApiStore()\nconst envStore = useEnvStore()\nconst appStore = useAppStore()\n\nconst isDarwin = envStore.env.os === 'darwin'\n\nconst pinWindow = () => {\n  isPinned.value = !isPinned.value\n  WindowSetAlwaysOnTop(isPinned.value)\n}\n\nconst closeWindow = async () => {\n  if (appSettingsStore.app.exitOnClose) {\n    exitApp()\n  } else {\n    WindowHide()\n  }\n}\n\nconst menus: Menu[] = [\n  {\n    label: 'titlebar.resetSize',\n    handler: () => WindowSetSize(800, 540),\n  },\n  {\n    label: 'titlebar.reload',\n    handler: reloadApp,\n  },\n  {\n    label: 'titlebar.restart',\n    handler: RestartApp,\n  },\n  {\n    label: 'titlebar.exitApp',\n    handler: exitApp,\n  },\n]\n\nconst onResize = debounce(async () => {\n  isMaximised.value = await WindowIsMaximised()\n}, 100)\n\nonMounted(() => window.addEventListener('resize', onResize))\nonUnmounted(() => window.removeEventListener('resize', onResize))\n</script>\n\n<template>\n  <div v-menu=\"menus\" class=\"flex items-center py-8 gap-8 px-12\" style=\"--wails-draggable: drag\">\n    <img v-if=\"!isDarwin\" class=\"w-24 h-24\" draggable=\"false\" :src=\"logo\" />\n\n    <div\n      :class=\"isDarwin ? 'justify-center py-4 text-12' : 'text-14'\"\n      :style=\"{\n        color: kernelApiStore.running ? 'var(--primary-color)' : 'var(--color)',\n      }\"\n      class=\"font-bold w-full h-full flex items-center\"\n      @dblclick=\"WindowToggleMaximise\"\n    >\n      {{ APP_TITLE }} {{ APP_VERSION }}\n      <CustomAction :actions=\"appStore.customActions.title_bar\" />\n      <Icon\n        v-if=\"kernelApiStore.starting || kernelApiStore.stopping || kernelApiStore.restarting\"\n        :size=\"14\"\n        icon=\"loading\"\n        class=\"rotation mx-4\"\n      />\n    </div>\n\n    <div\n      v-if=\"!isDarwin\"\n      class=\"ml-auto flex items-center gap-4\"\n      style=\"--wails-draggable: disabled\"\n    >\n      <Button type=\"text\" :icon=\"isPinned ? 'pinFill' : 'pin'\" @click.stop=\"pinWindow\" />\n      <Button icon=\"minimize\" type=\"text\" @click.stop=\"WindowMinimise\" />\n      <Button\n        :icon=\"isMaximised ? 'maximize2' : 'maximize'\"\n        type=\"text\"\n        @click.stop=\"WindowToggleMaximise\"\n      />\n      <Button\n        :class=\"{ 'hover:!bg-red': appSettingsStore.app.exitOnClose }\"\n        :loading=\"appStore.isAppExiting || appStore.isAppReloading\"\n        icon=\"close\"\n        type=\"text\"\n        @click.stop=\"closeWindow\"\n      />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/components/components.d.ts",
    "content": "export {}\n\ndeclare module 'vue' {\n  export interface GlobalComponents {\n    Button: (typeof import('./Button/index.vue'))['default']\n    Card: (typeof import('./Card/index.vue'))['default']\n    CheckBox: (typeof import('./CheckBox/index.vue'))['default']\n    CodeViewer: (typeof import('./CodeViewer/index.vue'))['default']\n    ColorPicker: (typeof import('./ColorPicker/index.vue'))['default']\n    Confirm: (typeof import('./Confirm/index.vue'))['default']\n    CustomAction: (typeof import('./CustomAction/index.vue'))['default']\n    Divider: (typeof import('./Divider/index.vue'))['default']\n    Dropdown: (typeof import('./Dropdown/index.vue'))['default']\n    Empty: (typeof import('./Empty/index.vue'))['default']\n    Icon: (typeof import('./Icon/index.vue'))['default']\n    Input: (typeof import('./Input/index.vue'))['default']\n    InputList: (typeof import('./InputList/index.vue'))['default']\n    InterfaceSelect: (typeof import('./InterfaceSelect/index.vue'))['default']\n    KeyValueEditor: (typeof import('./KeyValueEditor/index.vue'))['default']\n    Menu: (typeof import('./Menu/index.vue'))['default']\n    Message: (typeof import('./Message/index.vue'))['default']\n    Modal: (typeof import('./Modal/index.vue'))['default']\n    MultipleSelect: (typeof import('./Select/index.vue'))['default']\n    Pagination: (typeof import('./Pagination/index.vue'))['default']\n    Picker: (typeof import('./Picker/index.vue'))['default']\n    Progress: (typeof import('./Progress/index.vue'))['default']\n    Prompt: (typeof import('./Prompt/index.vue'))['default']\n    Radio: (typeof import('./Radio/index.vue'))['default']\n    Select: (typeof import('./Select/index.vue'))['default']\n    Switch: (typeof import('./Switch/index.vue'))['default']\n    Table: (typeof import('./Table/index.vue'))['default']\n    Tabs: (typeof import('./Tabs/index.vue'))['default']\n    Tag: (typeof import('./Tag/index.vue'))['default']\n    Tips: (typeof import('./Tips/index.vue'))['default']\n    TrafficChart: (typeof import('./TrafficChart/index.vue'))['default']\n  }\n}\n"
  },
  {
    "path": "frontend/src/components/index.ts",
    "content": "import type { Plugin, App, Component } from 'vue'\n\nexport { default as TitleBar } from './_common/TitleBar.vue'\nexport { default as NavigationBar } from './_common/NavigationBar.vue'\nexport { default as SplashView } from './_common/SplashView.vue'\nexport { default as AboutView } from './_common/AboutView.vue'\nexport { default as CommandView } from './_common/CommandView.vue'\n\nconst Components = import.meta.glob<Component>('./*/index.vue', {\n  eager: true,\n  import: 'default',\n})\n\nexport default {\n  install: (app: App) => {\n    Object.entries(Components).forEach(([path, comp]) => {\n      const name = (path.split('/') as [string, string])[1]\n      app.component(name, comp)\n    })\n  },\n} as Plugin\n"
  },
  {
    "path": "frontend/src/constant/app.ts",
    "content": "import {\n  Color,\n  ControllerCloseMode,\n  Lang,\n  PluginTrigger,\n  RequestMethod,\n  ScheduledTasksType,\n  Theme,\n  View,\n  WebviewGpuPolicy,\n  WindowStartState,\n} from '@/enums/app'\n\nexport const LocalesFilePath = 'data/locales'\n\nexport const UserFilePath = 'data/user.yaml'\n\nexport const ProfilesFilePath = 'data/profiles.yaml'\n\nexport const SubscribesFilePath = 'data/subscribes.yaml'\n\nexport const RulesetsFilePath = 'data/rulesets.yaml'\n\nexport const PluginsFilePath = 'data/plugins.yaml'\n\nexport const ScheduledTasksFilePath = 'data/scheduledtasks.yaml'\n\nexport const PluginHubFilePath = 'data/.cache/plugin-list.json'\n\nexport const RulesetHubFilePath = 'data/.cache/ruleset-list.json'\n\nexport const RollingReleaseDirectory = 'data/rolling-release'\n\nexport const DefaultFontFamily =\n  'system-ui, \"Microsoft YaHei UI\", \"Source Han Sans CN\", \"Twemoji Mozilla\", sans-serif'\n\nexport const Colors = {\n  [Color.Default]: {\n    primary: 'rgb(0, 89, 214)',\n    secondary: 'rgb(5, 62, 142)',\n  },\n  [Color.Green]: {\n    primary: 'green',\n    secondary: '#025f02',\n  },\n  [Color.Purple]: {\n    primary: 'purple',\n    secondary: '#6a0f9c',\n  },\n  [Color.Custom]: {\n    primary: '#000',\n    secondary: '#000',\n  },\n}\n\nexport const LanguageOptions = [\n  { label: 'settings.lang.zh', value: Lang.ZH },\n  { label: 'settings.lang.en', value: Lang.EN },\n]\n\nexport const ViewOptions = [\n  { label: 'common.grid', value: View.Grid },\n  { label: 'common.list', value: View.List },\n]\n\nexport const ControllerCloseModeOptions = [\n  { label: 'home.controller.closeMode.all', value: ControllerCloseMode.All },\n  { label: 'home.controller.closeMode.button', value: ControllerCloseMode.Button },\n]\n\nexport const RequestMethodOptions = [\n  { label: RequestMethod.Get, value: RequestMethod.Get },\n  { label: RequestMethod.Post, value: RequestMethod.Post },\n  { label: RequestMethod.Delete, value: RequestMethod.Delete },\n  { label: RequestMethod.Put, value: RequestMethod.Put },\n  { label: RequestMethod.Head, value: RequestMethod.Head },\n  { label: RequestMethod.Patch, value: RequestMethod.Patch },\n]\n\nexport const ThemeOptions = [\n  {\n    label: 'settings.theme.dark',\n    value: Theme.Dark,\n  },\n  {\n    label: 'settings.theme.light',\n    value: Theme.Light,\n  },\n  {\n    label: 'settings.theme.auto',\n    value: Theme.Auto,\n  },\n]\n\nexport const ColorOptions = [\n  {\n    label: 'settings.color.default',\n    value: Color.Default,\n  },\n  {\n    label: 'settings.color.green',\n    value: Color.Green,\n  },\n  {\n    label: 'settings.color.purple',\n    value: Color.Purple,\n  },\n  {\n    label: 'settings.color.custom',\n    value: Color.Custom,\n  },\n]\n\nexport const WindowStateOptions = [\n  { label: 'settings.windowState.normal', value: WindowStartState.Normal },\n  { label: 'settings.windowState.minimised', value: WindowStartState.Minimised },\n]\n\nexport const WebviewGpuPolicyOptions = [\n  { label: 'settings.webviewGpuPolicy.always', value: WebviewGpuPolicy.Always },\n  { label: 'settings.webviewGpuPolicy.onDemand', value: WebviewGpuPolicy.OnDemand },\n  { label: 'settings.webviewGpuPolicy.never', value: WebviewGpuPolicy.Never },\n]\n\n// vue-draggable-plus config\nexport const DraggableOptions = {\n  animation: 150,\n}\n\nexport const PluginsTriggerOptions = [\n  { label: 'plugin.on::startup', value: PluginTrigger.OnStartup },\n  { label: 'plugin.on::ready', value: PluginTrigger.OnReady },\n  { label: 'plugin.on::reload', value: PluginTrigger.OnReload },\n  { label: 'plugin.on::shutdown', value: PluginTrigger.OnShutdown },\n  { label: 'plugin.on::manual', value: PluginTrigger.OnManual },\n  { label: 'plugin.on::generate', value: PluginTrigger.OnGenerate },\n  { label: 'plugin.on::subscribe', value: PluginTrigger.OnSubscribe },\n  { label: 'plugin.on::tray::update', value: PluginTrigger.OnTrayUpdate },\n  { label: 'plugin.on::before::core::start', value: PluginTrigger.OnBeforeCoreStart },\n  { label: 'plugin.on::core::started', value: PluginTrigger.OnCoreStarted },\n  { label: 'plugin.on::before::core::stop', value: PluginTrigger.OnBeforeCoreStop },\n  { label: 'plugin.on::core::stopped', value: PluginTrigger.OnCoreStopped },\n]\n\nexport const ScheduledTaskOptions = [\n  { label: 'scheduledtask.update::subscription', value: ScheduledTasksType.UpdateSubscription },\n  { label: 'scheduledtask.update::ruleset', value: ScheduledTasksType.UpdateRuleset },\n  { label: 'scheduledtask.update::plugin', value: ScheduledTasksType.UpdatePlugin },\n  { label: 'scheduledtask.run::plugin', value: ScheduledTasksType.RunPlugin },\n  { label: 'scheduledtask.run::script', value: ScheduledTasksType.RunScript },\n  {\n    label: 'scheduledtask.update::all::subscription',\n    value: ScheduledTasksType.UpdateAllSubscription,\n  },\n  { label: 'scheduledtask.update::all::ruleset', value: ScheduledTasksType.UpdateAllRuleset },\n  { label: 'scheduledtask.update::all::plugin', value: ScheduledTasksType.UpdateAllPlugin },\n]\n\nexport const DefaultSubscribeScript = `const onSubscribe = async (proxies, subscription) => {\\n  return { proxies, subscription }\\n}`\n\nexport const DefaultTestURL = 'https://www.gstatic.com/generate_204'\n\nexport const DefaultTestTimeout = 5000\n\nexport const DefaultConcurrencyLimit = 20\n\nexport const DefaultCardColumns = 5\n\nexport const DefaultControllerSensitivity = 2\n"
  },
  {
    "path": "frontend/src/constant/kernel.ts",
    "content": "import {\n  ClashMode,\n  Inbound,\n  Outbound,\n  TunStack,\n  LogLevel,\n  RuleType,\n  RulesetFormat,\n  RulesetType,\n  RuleAction,\n  Sniffer,\n  Strategy,\n  RuleActionReject,\n  DnsServer,\n} from '@/enums/kernel'\n\nexport const CoreStopOutputKeyword = 'sing-box started'\nexport const CoreWorkingDirectory = 'data/sing-box'\nexport const CorePidFilePath = CoreWorkingDirectory + '/pid.txt'\nexport const CoreConfigFilePath = CoreWorkingDirectory + '/config.json'\nexport const CoreCacheFilePath = CoreWorkingDirectory + '/cache.db'\n\nexport const ModeOptions = [\n  {\n    label: 'kernel.global',\n    value: ClashMode.Global,\n    desc: 'kernel.globalDesc',\n  },\n  {\n    label: 'kernel.rule',\n    value: ClashMode.Rule,\n    desc: 'kernel.ruleDesc',\n  },\n  {\n    label: 'kernel.direct',\n    value: ClashMode.Direct,\n    desc: 'kernel.directDesc',\n  },\n]\n\nexport const LogLevelOptions = [\n  {\n    label: 'kernel.log.trace',\n    value: LogLevel.Trace,\n  },\n  {\n    label: 'kernel.log.debug',\n    value: LogLevel.Debug,\n  },\n  {\n    label: 'kernel.log.info',\n    value: LogLevel.Info,\n  },\n  {\n    label: 'kernel.log.warn',\n    value: LogLevel.Warn,\n  },\n  {\n    label: 'kernel.log.error',\n    value: LogLevel.Error,\n  },\n  {\n    label: 'kernel.log.fatal',\n    value: LogLevel.Fatal,\n  },\n  {\n    label: 'kernel.log.panic',\n    value: LogLevel.Panic,\n  },\n]\n\nexport const InboundOptions = [\n  { label: 'mixed', value: Inbound.Mixed },\n  { label: 'socks', value: Inbound.Socks },\n  { label: 'http', value: Inbound.Http },\n  { label: 'tun', value: Inbound.Tun },\n]\n\nexport const OutboundOptions = [\n  { label: 'kernel.outbounds.direct', value: Outbound.Direct },\n  { label: 'kernel.outbounds.block', value: Outbound.Block },\n  { label: 'kernel.outbounds.selector', value: Outbound.Selector },\n  { label: 'kernel.outbounds.urltest', value: Outbound.Urltest },\n]\n\nexport const RulesTypeOptions = [\n  {\n    label: 'kernel.rules.type.inbound',\n    value: RuleType.Inbound,\n  },\n  {\n    label: 'kernel.rules.type.network',\n    value: RuleType.Network,\n  },\n  {\n    label: 'kernel.rules.type.protocol',\n    value: RuleType.Protocol,\n  },\n  {\n    label: 'kernel.rules.type.domain',\n    value: RuleType.Domain,\n  },\n  {\n    label: 'kernel.rules.type.domain_suffix',\n    value: RuleType.DomainSuffix,\n  },\n  {\n    label: 'kernel.rules.type.domain_keyword',\n    value: RuleType.DomainKeyword,\n  },\n  {\n    label: 'kernel.rules.type.domain_regex',\n    value: RuleType.DomainRegex,\n  },\n  {\n    label: 'kernel.rules.type.source_ip_cidr',\n    value: RuleType.SourceIPCidr,\n  },\n  {\n    label: 'kernel.rules.type.ip_cidr',\n    value: RuleType.IPCidr,\n  },\n  {\n    label: 'kernel.rules.type.ip_is_private',\n    value: RuleType.IpIsPrivate,\n  },\n  {\n    label: 'kernel.rules.type.source_port',\n    value: RuleType.SourcePort,\n  },\n  {\n    label: 'kernel.rules.type.source_port_range',\n    value: RuleType.SourcePortRange,\n  },\n  {\n    label: 'kernel.rules.type.port',\n    value: RuleType.Port,\n  },\n  {\n    label: 'kernel.rules.type.port_range',\n    value: RuleType.PortRange,\n  },\n  {\n    label: 'kernel.rules.type.process_name',\n    value: RuleType.ProcessName,\n  },\n  {\n    label: 'kernel.rules.type.process_path',\n    value: RuleType.ProcessPath,\n  },\n  {\n    label: 'kernel.rules.type.process_path_regex',\n    value: RuleType.ProcessPathRegex,\n  },\n  {\n    label: 'kernel.rules.type.clash_mode',\n    value: RuleType.ClashMode,\n  },\n  {\n    label: 'kernel.rules.type.rule_set',\n    value: RuleType.RuleSet,\n  },\n  {\n    label: 'kernel.rules.type.inline',\n    value: RuleType.Inline,\n  },\n]\n\nexport const DnsRuleTypeOptions = RulesTypeOptions.concat([\n  {\n    label: 'kernel.rules.type.ip_accept_any',\n    value: RuleType.IpAcceptAny,\n  },\n])\n\nexport const TunStackOptions = [\n  { label: 'kernel.inbounds.tun.system', value: TunStack.System },\n  { label: 'kernel.inbounds.tun.gvisor', value: TunStack.GVisor },\n  { label: 'kernel.inbounds.tun.mixed', value: TunStack.Mixed },\n]\n\nexport const RulesetTypeOptions = [\n  { label: 'kernel.route.rule_set.type.inline', value: RulesetType.Inline },\n  { label: 'kernel.route.rule_set.type.local', value: RulesetType.Local },\n  { label: 'kernel.route.rule_set.type.remote', value: RulesetType.Remote },\n]\n\nexport const RulesetFormatOptions = [\n  { label: 'ruleset.format.source', value: RulesetFormat.Source },\n  { label: 'ruleset.format.binary', value: RulesetFormat.Binary },\n]\n\nexport const DomainStrategyOptions = [\n  { label: 'kernel.strategy.default', value: Strategy.Default },\n  { label: 'kernel.strategy.prefer_ipv4', value: Strategy.PreferIPv4 },\n  { label: 'kernel.strategy.prefer_ipv6', value: Strategy.PreferIPv6 },\n  { label: 'kernel.strategy.ipv4_only', value: Strategy.IPv4Only },\n  { label: 'kernel.strategy.ipv6_only', value: Strategy.IPv6Only },\n]\n\nexport const RuleActionOptions = [\n  { label: 'kernel.route.rules.action.route', value: RuleAction.Route },\n  { label: 'kernel.route.rules.action.route-options', value: RuleAction.RouteOptions },\n  { label: 'kernel.route.rules.action.reject', value: RuleAction.Reject },\n  { label: 'kernel.route.rules.action.hijack-dns', value: RuleAction.HijackDNS },\n  { label: 'kernel.route.rules.action.sniff', value: RuleAction.Sniff },\n  { label: 'kernel.route.rules.action.resolve', value: RuleAction.Resolve },\n]\n\nexport const RuleActionRejectOptions = [\n  { label: 'kernel.route.rules.action.rejectDefault', value: RuleActionReject.Default },\n  { label: 'kernel.route.rules.action.rejectDrop', value: RuleActionReject.Drop },\n  { label: 'kernel.route.rules.action.rejectReply', value: RuleActionReject.Reply },\n]\n\nexport const DnsServerTypeOptions = [\n  { label: 'kernel.dns.type.local', value: DnsServer.Local },\n  { label: 'kernel.dns.type.hosts', value: DnsServer.Hosts },\n  { label: 'kernel.dns.type.tcp', value: DnsServer.Tcp },\n  { label: 'kernel.dns.type.udp', value: DnsServer.Udp },\n  { label: 'kernel.dns.type.tls', value: DnsServer.Tls },\n  { label: 'kernel.dns.type.quic', value: DnsServer.Quic },\n  { label: 'kernel.dns.type.https', value: DnsServer.Https },\n  { label: 'kernel.dns.type.h3', value: DnsServer.H3 },\n  { label: 'kernel.dns.type.dhcp', value: DnsServer.Dhcp },\n  { label: 'kernel.dns.type.fakeip', value: DnsServer.FakeIP },\n]\n\nexport const DnsRuleActionOptions = [\n  { label: 'kernel.route.rules.action.route', value: RuleAction.Route },\n  { label: 'kernel.route.rules.action.route-options', value: RuleAction.RouteOptions },\n  { label: 'kernel.route.rules.action.reject', value: RuleAction.Reject },\n  { label: 'kernel.route.rules.action.predefined', value: RuleAction.Predefined },\n]\n\nexport const DnsRuleActionRejectOptions = [\n  { label: 'kernel.route.rules.action.rejectDefault', value: RuleActionReject.Default },\n  { label: 'kernel.route.rules.action.rejectDrop', value: RuleActionReject.Drop },\n]\n\nexport const RuleSnifferOptions = [\n  { label: 'kernel.route.rules.sniffer.http', value: Sniffer.Http },\n  { label: 'kernel.route.rules.sniffer.tls', value: Sniffer.Tls },\n  { label: 'kernel.route.rules.sniffer.quic', value: Sniffer.Quic },\n  { label: 'kernel.route.rules.sniffer.stun', value: Sniffer.Stun },\n  { label: 'kernel.route.rules.sniffer.dns', value: Sniffer.Dns },\n  { label: 'kernel.route.rules.sniffer.bittorrent', value: Sniffer.Bittorrent },\n  { label: 'kernel.route.rules.sniffer.dtls', value: Sniffer.Dtls },\n  { label: 'kernel.route.rules.sniffer.ssh', value: Sniffer.Ssh },\n  { label: 'kernel.route.rules.sniffer.rdp', value: Sniffer.Rdp },\n  { label: 'kernel.route.rules.sniffer.ntp', value: Sniffer.Ntp },\n]\n\nexport const EmptyRuleSet = {\n  version: 1,\n  rules: [],\n}\n\nexport const DefaultExcludeProtocols = 'direct|reject|selector|urltest|block|dns|shadowsocksr'\n\nexport const BuiltInOutbound = [Outbound.Direct, Outbound.Block]\n\nexport const DefaultConnections = () => {\n  return {\n    visibility: {\n      'metadata.type': true,\n      'metadata.processPath': false,\n      'metadata.host': true,\n      'metadata.sourceIP': false,\n      'metadata.destinationIP': false,\n      rule: true,\n      chains: true,\n      up: true,\n      down: true,\n      upload: true,\n      download: true,\n      start: true,\n    },\n    order: [\n      'metadata.type',\n      'metadata.processPath',\n      'metadata.host',\n      'metadata.sourceIP',\n      'metadata.destinationIP',\n      'rule',\n      'chains',\n      'up',\n      'down',\n      'upload',\n      'download',\n      'start',\n    ],\n  }\n}\n\nexport const DefaultCoreConfig = () => {\n  return {\n    env: {},\n    args: [\n      'run',\n      '--disable-color',\n      '-c',\n      '$APP_BASE_PATH/$CORE_BASE_PATH/config.json',\n      '-D',\n      '$APP_BASE_PATH/$CORE_BASE_PATH',\n    ],\n  }\n}\n"
  },
  {
    "path": "frontend/src/constant/profile.ts",
    "content": "import {\n  LogLevel,\n  Inbound,\n  Outbound,\n  TunStack,\n  ClashMode,\n  RulesetType,\n  RulesetFormat,\n  RuleType,\n  RuleAction,\n  Strategy,\n  DnsServer,\n} from '@/enums/kernel'\nimport i18n from '@/lang'\nimport { generateSecureKey, sampleID } from '@/utils'\n\nimport { DefaultTestURL } from './app'\n\nconst { t } = i18n.global\n\nconst DefaultOutboundIds = {\n  Select: 'outbound-select',\n  Urltest: 'outbound-urlte',\n  Direct: 'outbound-direct',\n  Block: 'outbound-block',\n  Fallback: 'outbound-fallback',\n  Global: 'outbound-global',\n}\n\nconst DefaultInboundIds = {\n  MixedIn: 'mixed-in',\n  Tun: 'tun-in',\n}\n\nconst DefaultRulesetIds = {\n  CATEGORY_ADS: 'Category-Ads',\n  GEOIP_CN: 'GeoIP-CN',\n  GEOSITE_CN: 'GeoSite-CN',\n  GEOLOCATION_NOT_CN: 'GeoLocation-!CN',\n  GEOSITE_PRIVATE: 'GeoSite-Private',\n  GEOIP_PRIVATE: 'GeoIP-Private',\n}\n\nconst DefaultDnsServersIds = {\n  LocalDns: 'Local-DNS',\n  RemoteDns: 'Remote-DNS',\n  FakeIP: 'Fake-IP',\n  LocalDnsResolver: 'Local-DNS-Resolver',\n  RemoteDnsResolver: 'Remote-DNS-Resolver',\n}\n\nexport const DefaultLog = (): ILog => ({\n  disabled: false,\n  level: LogLevel.Info,\n  output: '',\n  timestamp: false,\n})\n\nexport const DefaultExperimental = (): IExperimental => ({\n  clash_api: {\n    external_controller: '127.0.0.1:20123',\n    external_ui: '',\n    external_ui_download_url: '',\n    external_ui_download_detour: DefaultOutboundIds.Direct,\n    secret: generateSecureKey(),\n    default_mode: ClashMode.Rule,\n    access_control_allow_origin: ['*'],\n    access_control_allow_private_network: false,\n  },\n  cache_file: {\n    enabled: true,\n    path: 'cache.db',\n    cache_id: sampleID(),\n    store_fakeip: true,\n    store_rdrc: true,\n    rdrc_timeout: '7d',\n  },\n})\n\nexport const DefaultInboundSocks = (): NonNullable<IInbound['socks']> => ({\n  listen: {\n    listen: '127.0.0.1',\n    listen_port: 20120,\n    tcp_fast_open: false,\n    tcp_multi_path: false,\n    udp_fragment: false,\n  },\n  users: [],\n})\n\nexport const DefaultInboundHttp = (): NonNullable<IInbound['http']> => ({\n  listen: {\n    listen: '127.0.0.1',\n    listen_port: 20121,\n    tcp_fast_open: false,\n    tcp_multi_path: false,\n    udp_fragment: false,\n  },\n  users: [],\n})\n\nexport const DefaultInboundMixed = (): NonNullable<IInbound['mixed']> => ({\n  listen: {\n    listen: '127.0.0.1',\n    listen_port: 20122,\n    tcp_fast_open: false,\n    tcp_multi_path: false,\n    udp_fragment: false,\n  },\n  users: [],\n})\n\nexport const DefaultInboundTun = (): NonNullable<IInbound['tun']> => ({\n  interface_name: '',\n  address: ['172.18.0.1/30', 'fdfe:dcba:9876::1/126'],\n  mtu: 0,\n  auto_route: true,\n  strict_route: true,\n  route_address: [],\n  route_exclude_address: [],\n  endpoint_independent_nat: false,\n  stack: TunStack.Mixed,\n})\n\nexport const DefaultInbounds = (): IInbound[] => [\n  {\n    id: DefaultInboundIds.MixedIn,\n    type: Inbound.Mixed,\n    tag: 'mixed-in',\n    enable: true,\n    mixed: DefaultInboundMixed(),\n  },\n  {\n    id: DefaultInboundIds.Tun,\n    type: Inbound.Tun,\n    tag: 'tun-in',\n    enable: false,\n    tun: DefaultInboundTun(),\n  },\n]\n\nexport const DefaultOutbound = (): IOutbound => ({\n  id: sampleID(),\n  tag: '',\n  type: Outbound.Selector,\n  outbounds: [],\n  interrupt_exist_connections: true,\n  url: DefaultTestURL,\n  interval: '3m',\n  tolerance: 150,\n  include: '',\n  exclude: '',\n})\n\nexport const DefaultOutbounds = (): IOutbound[] => [\n  {\n    id: DefaultOutboundIds.Select,\n    tag: t('outbound.select'),\n    type: Outbound.Selector,\n    outbounds: [{ id: DefaultOutboundIds.Urltest, type: 'Built-in', tag: t('outbound.urltest') }],\n    interrupt_exist_connections: true,\n    url: '',\n    interval: '3m',\n    tolerance: 150,\n    include: '',\n    exclude: '',\n  },\n  {\n    id: DefaultOutboundIds.Urltest,\n    tag: t('outbound.urltest'),\n    type: Outbound.Urltest,\n    outbounds: [],\n    interrupt_exist_connections: true,\n    url: DefaultTestURL,\n    interval: '3m',\n    tolerance: 150,\n    include: '',\n    exclude: '',\n  },\n  {\n    id: DefaultOutboundIds.Direct,\n    tag: t('outbound.direct'),\n    type: Outbound.Selector,\n    outbounds: [\n      { id: 'direct', type: 'Built-in', tag: 'direct' },\n      { id: 'block', type: 'Built-in', tag: 'block' },\n    ],\n    interrupt_exist_connections: true,\n    url: '',\n    interval: '3m',\n    tolerance: 150,\n    include: '',\n    exclude: '',\n  },\n  {\n    id: DefaultOutboundIds.Block,\n    tag: t('outbound.block'),\n    type: Outbound.Selector,\n    outbounds: [\n      { id: 'block', type: 'Built-in', tag: 'block' },\n      { id: 'direct', type: 'Built-in', tag: 'direct' },\n    ],\n    interrupt_exist_connections: true,\n    url: '',\n    interval: '3m',\n    tolerance: 150,\n    include: '',\n    exclude: '',\n  },\n  {\n    id: DefaultOutboundIds.Fallback,\n    tag: t('outbound.fallback'),\n    type: Outbound.Selector,\n    outbounds: [\n      { id: DefaultOutboundIds.Select, type: 'Built-in', tag: t('outbound.select') },\n      { id: DefaultOutboundIds.Direct, type: 'Built-in', tag: t('outbound.direct') },\n    ],\n    interrupt_exist_connections: true,\n    url: '',\n    interval: '3m',\n    tolerance: 150,\n    include: '',\n    exclude: '',\n  },\n  {\n    id: DefaultOutboundIds.Global,\n    tag: 'GLOBAL',\n    type: Outbound.Selector,\n    outbounds: [\n      { id: DefaultOutboundIds.Select, type: 'Built-in', tag: t('outbound.select') },\n      { id: DefaultOutboundIds.Urltest, type: 'Built-in', tag: t('outbound.urltest') },\n      { id: DefaultOutboundIds.Direct, type: 'Built-in', tag: t('outbound.direct') },\n      { id: DefaultOutboundIds.Block, type: 'Built-in', tag: t('outbound.block') },\n      { id: DefaultOutboundIds.Fallback, type: 'Built-in', tag: t('outbound.fallback') },\n    ],\n    interrupt_exist_connections: true,\n    url: '',\n    interval: '3m',\n    tolerance: 150,\n    include: '',\n    exclude: '',\n  },\n]\n\nexport const DefaultRouteRule = (): IRule => ({\n  id: sampleID(),\n  type: RuleType.RuleSet,\n  enable: true,\n  payload: '',\n  invert: false,\n  action: RuleAction.Route,\n  outbound: '',\n  sniffer: [],\n  strategy: Strategy.Default,\n  server: '',\n})\n\nexport const DefaultRouteRuleset = (): IRuleSet => ({\n  id: sampleID(),\n  type: RulesetType.Local,\n  tag: '',\n  format: RulesetFormat.Binary,\n  url: '',\n  download_detour: '',\n  update_interval: '',\n  rules: '',\n  path: '',\n})\n\nexport const DefaultRoute = (): IRoute => ({\n  rules: [\n    {\n      id: sampleID(),\n      type: RuleType.Inbound,\n      payload: DefaultInboundIds.Tun,\n      enable: true,\n      invert: false,\n      action: RuleAction.Sniff,\n      outbound: '',\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.Protocol,\n      enable: true,\n      payload: 'dns',\n      invert: false,\n      action: RuleAction.HijackDNS,\n      outbound: '',\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.ClashMode,\n      payload: ClashMode.Direct,\n      enable: true,\n      invert: false,\n      action: RuleAction.Route,\n      outbound: DefaultOutboundIds.Direct,\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.ClashMode,\n      enable: true,\n      payload: ClashMode.Global,\n      invert: false,\n      action: RuleAction.Route,\n      outbound: DefaultOutboundIds.Global,\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: RuleType.InsertionPoint,\n      type: RuleType.InsertionPoint,\n      enable: true,\n      payload: '',\n      invert: false,\n      action: RuleAction.Route,\n      outbound: '',\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.Network,\n      enable: true,\n      payload: 'icmp',\n      invert: false,\n      action: RuleAction.Route,\n      outbound: DefaultOutboundIds.Direct,\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.Protocol,\n      enable: true,\n      payload: 'quic',\n      invert: false,\n      action: RuleAction.Route,\n      outbound: DefaultOutboundIds.Block,\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.RuleSet,\n      enable: true,\n      payload: DefaultRulesetIds.CATEGORY_ADS,\n      invert: false,\n      action: RuleAction.Route,\n      outbound: DefaultOutboundIds.Block,\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.RuleSet,\n      enable: true,\n      payload: DefaultRulesetIds.GEOSITE_PRIVATE,\n      invert: false,\n      action: RuleAction.Route,\n      outbound: DefaultOutboundIds.Direct,\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.RuleSet,\n      enable: true,\n      payload: DefaultRulesetIds.GEOSITE_CN,\n      invert: false,\n      action: RuleAction.Route,\n      outbound: DefaultOutboundIds.Direct,\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.RuleSet,\n      enable: true,\n      payload: DefaultRulesetIds.GEOIP_PRIVATE,\n      invert: false,\n      action: RuleAction.Route,\n      outbound: DefaultOutboundIds.Direct,\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.RuleSet,\n      enable: true,\n      payload: DefaultRulesetIds.GEOIP_CN,\n      invert: false,\n      action: RuleAction.Route,\n      outbound: DefaultOutboundIds.Direct,\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n    {\n      id: sampleID(),\n      type: RuleType.RuleSet,\n      enable: true,\n      payload: DefaultRulesetIds.GEOLOCATION_NOT_CN,\n      invert: false,\n      action: RuleAction.Route,\n      outbound: DefaultOutboundIds.Select,\n      sniffer: [],\n      strategy: Strategy.Default,\n      server: '',\n    },\n  ],\n  rule_set: [\n    {\n      id: DefaultRulesetIds.CATEGORY_ADS,\n      type: RulesetType.Remote,\n      tag: DefaultRulesetIds.CATEGORY_ADS,\n      format: RulesetFormat.Binary,\n      url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs',\n      download_detour: DefaultOutboundIds.Direct,\n      update_interval: '',\n      rules: '',\n      path: '',\n    },\n    {\n      id: DefaultRulesetIds.GEOIP_PRIVATE,\n      type: RulesetType.Remote,\n      tag: DefaultRulesetIds.GEOIP_PRIVATE,\n      format: RulesetFormat.Binary,\n      url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/private.srs',\n      download_detour: DefaultOutboundIds.Direct,\n      update_interval: '',\n      rules: '',\n      path: '',\n    },\n    {\n      id: DefaultRulesetIds.GEOSITE_PRIVATE,\n      type: RulesetType.Remote,\n      tag: DefaultRulesetIds.GEOSITE_PRIVATE,\n      format: RulesetFormat.Binary,\n      url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs',\n      download_detour: DefaultOutboundIds.Direct,\n      update_interval: '',\n      rules: '',\n      path: '',\n    },\n    {\n      id: DefaultRulesetIds.GEOIP_CN,\n      type: RulesetType.Remote,\n      tag: DefaultRulesetIds.GEOIP_CN,\n      format: RulesetFormat.Binary,\n      url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs',\n      download_detour: DefaultOutboundIds.Direct,\n      update_interval: '',\n      rules: '',\n      path: '',\n    },\n    {\n      id: DefaultRulesetIds.GEOSITE_CN,\n      type: RulesetType.Remote,\n      tag: DefaultRulesetIds.GEOSITE_CN,\n      format: RulesetFormat.Binary,\n      url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs',\n      download_detour: DefaultOutboundIds.Direct,\n      update_interval: '',\n      rules: '',\n      path: '',\n    },\n    {\n      id: DefaultRulesetIds.GEOLOCATION_NOT_CN,\n      type: RulesetType.Remote,\n      tag: DefaultRulesetIds.GEOLOCATION_NOT_CN,\n      format: RulesetFormat.Binary,\n      url: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/geolocation-!cn.srs',\n      download_detour: DefaultOutboundIds.Direct,\n      update_interval: '',\n      rules: '',\n      path: '',\n    },\n  ],\n  auto_detect_interface: true,\n  default_interface: '',\n  final: DefaultOutboundIds.Fallback,\n  find_process: false,\n  default_domain_resolver: {\n    server: DefaultDnsServersIds.LocalDns,\n    client_subnet: '',\n  },\n})\n\nexport const DefaultDnsServer = (): IDNSServer => ({\n  id: sampleID(),\n  tag: '',\n  type: DnsServer.Local,\n  detour: '',\n  domain_resolver: '',\n  server: '',\n  server_port: '',\n  path: '',\n  interface: '',\n  inet4_range: '',\n  inet6_range: '',\n  hosts_path: [],\n  predefined: {},\n})\n\nexport const DefaultDnsServers = (): IDNSServer[] => [\n  {\n    id: DefaultDnsServersIds.FakeIP,\n    tag: DefaultDnsServersIds.FakeIP,\n    detour: '',\n    type: DnsServer.FakeIP,\n    domain_resolver: '',\n    server: '',\n    server_port: '',\n    path: '',\n    interface: '',\n    inet4_range: '198.18.0.0/15',\n    inet6_range: 'fc00::/18',\n    hosts_path: [],\n    predefined: {},\n  },\n  {\n    id: DefaultDnsServersIds.LocalDns,\n    tag: DefaultDnsServersIds.LocalDns,\n    detour: '',\n    type: DnsServer.Https,\n    domain_resolver: DefaultDnsServersIds.LocalDnsResolver,\n    server: '223.5.5.5',\n    server_port: '443',\n    path: '/dns-query',\n    interface: '',\n    inet4_range: '',\n    inet6_range: '',\n    hosts_path: [],\n    predefined: {},\n  },\n  {\n    id: DefaultDnsServersIds.LocalDnsResolver,\n    tag: DefaultDnsServersIds.LocalDnsResolver,\n    detour: '',\n    type: DnsServer.Udp,\n    domain_resolver: '',\n    server: '223.5.5.5',\n    server_port: '53',\n    path: '',\n    interface: '',\n    inet4_range: '',\n    inet6_range: '',\n    hosts_path: [],\n    predefined: {},\n  },\n  {\n    id: DefaultDnsServersIds.RemoteDns,\n    tag: DefaultDnsServersIds.RemoteDns,\n    detour: DefaultOutboundIds.Select,\n    type: DnsServer.Tls,\n    domain_resolver: DefaultDnsServersIds.RemoteDnsResolver,\n    server: '8.8.8.8',\n    server_port: '853',\n    path: '',\n    interface: '',\n    inet4_range: '',\n    inet6_range: '',\n    hosts_path: [],\n    predefined: {},\n  },\n  {\n    id: DefaultDnsServersIds.RemoteDnsResolver,\n    tag: DefaultDnsServersIds.RemoteDnsResolver,\n    detour: DefaultOutboundIds.Select,\n    type: DnsServer.Udp,\n    domain_resolver: '',\n    server: '8.8.8.8',\n    server_port: '53',\n    path: '',\n    interface: '',\n    inet4_range: '',\n    inet6_range: '',\n    hosts_path: [],\n    predefined: {},\n  },\n]\n\nexport const DefaultFakeIPDnsRule = () => ({\n  __is_fake_ip: true,\n  type: 'logical',\n  mode: 'and',\n  rules: [\n    {\n      domain_suffix: [\n        '.lan',\n        '.localdomain',\n        '.example',\n        '.invalid',\n        '.localhost',\n        '.test',\n        '.local',\n        '.home.arpa',\n        '.msftconnecttest.com',\n        '.msftncsi.com',\n      ],\n      invert: true,\n    },\n    {\n      query_type: ['A', 'AAAA'],\n    },\n  ],\n})\n\nexport const DefaultDnsRule = (): IDNSRule => ({\n  id: sampleID(),\n  type: RuleType.RuleSet,\n  enable: true,\n  payload: '',\n  action: RuleAction.Route,\n  invert: false,\n  // route\n  server: '',\n  strategy: Strategy.Default,\n  // route/route-options\n  disable_cache: false,\n  client_subnet: '',\n})\n\nexport const DefaultDnsRules = (): IDNSRule[] => [\n  {\n    id: sampleID(),\n    type: RuleType.ClashMode,\n    enable: true,\n    payload: ClashMode.Direct,\n    action: RuleAction.Route,\n    server: DefaultDnsServersIds.LocalDns,\n    invert: false,\n    strategy: Strategy.Default,\n    disable_cache: false,\n    client_subnet: '',\n  },\n  {\n    id: sampleID(),\n    type: RuleType.ClashMode,\n    enable: true,\n    payload: ClashMode.Global,\n    action: RuleAction.Route,\n    server: DefaultDnsServersIds.RemoteDns,\n    invert: false,\n    strategy: Strategy.Default,\n    disable_cache: false,\n    client_subnet: '',\n  },\n  {\n    id: RuleType.InsertionPoint,\n    type: RuleType.InsertionPoint,\n    enable: true,\n    payload: '',\n    action: RuleAction.Route,\n    server: '',\n    invert: false,\n    strategy: Strategy.Default,\n    disable_cache: false,\n    client_subnet: '',\n  },\n  {\n    id: sampleID(),\n    type: RuleType.RuleSet,\n    enable: true,\n    payload: DefaultRulesetIds.GEOSITE_CN,\n    action: RuleAction.Route,\n    server: DefaultDnsServersIds.LocalDns,\n    invert: false,\n    strategy: Strategy.Default,\n    disable_cache: false,\n    client_subnet: '',\n  },\n  {\n    id: sampleID(),\n    type: RuleType.Inline,\n    enable: false,\n    payload: JSON.stringify(DefaultFakeIPDnsRule(), null, 2),\n    action: RuleAction.Route,\n    server: DefaultDnsServersIds.FakeIP,\n    invert: false,\n    strategy: Strategy.Default,\n    disable_cache: false,\n    client_subnet: '',\n  },\n  {\n    id: sampleID(),\n    type: RuleType.RuleSet,\n    enable: true,\n    payload: DefaultRulesetIds.GEOLOCATION_NOT_CN,\n    action: RuleAction.Route,\n    server: DefaultDnsServersIds.RemoteDns,\n    invert: false,\n    strategy: Strategy.Default,\n    disable_cache: false,\n    client_subnet: '',\n  },\n]\n\nexport const DefaultDns = (): IDNS => ({\n  servers: DefaultDnsServers(),\n  rules: DefaultDnsRules(),\n  disable_cache: false,\n  disable_expire: false,\n  independent_cache: false,\n  client_subnet: '',\n  final: DefaultDnsServersIds.RemoteDns,\n  strategy: Strategy.Default,\n})\n\nexport const DefaultMixin = (): IProfile['mixin'] => {\n  return { priority: 'mixin', format: 'json', config: '' }\n}\n\nexport const DefaultScript = (): IProfile['script'] => {\n  return { code: `const onGenerate = async (config) => {\\n  return config\\n}` }\n}\n"
  },
  {
    "path": "frontend/src/directives/index.ts",
    "content": "import { vDraggable } from 'vue-draggable-plus'\n\nimport menu from './menu'\nimport platform from './platform'\nimport tips from './tips'\n\nimport type { Plugin, App } from 'vue'\n\nconst directives: any = {\n  menu,\n  tips,\n  platform,\n  draggable: vDraggable,\n}\n\nexport default {\n  install(app: App) {\n    Object.keys(directives).forEach((key) => {\n      app.directive(key, directives[key])\n    })\n  },\n} as Plugin\n"
  },
  {
    "path": "frontend/src/directives/menu.ts",
    "content": "import { useAppStore } from '@/stores'\nimport { sleep } from '@/utils'\n\nimport type { Directive, DirectiveBinding } from 'vue'\n\nconst updateMenus = (el: any, binding: DirectiveBinding) => {\n  const appStore = useAppStore()\n\n  el.oncontextmenu = async (e: MouseEvent) => {\n    e.preventDefault()\n    if (binding.value.length) {\n      appStore.menuPosition = { x: e.clientX, y: e.clientY }\n      appStore.menuList = binding.value\n      if (appStore.menuShow) {\n        appStore.menuShow = false\n        await sleep(200)\n      }\n      appStore.menuShow = true\n    }\n  }\n}\n\nexport default {\n  mounted(el: any, binding: DirectiveBinding) {\n    updateMenus(el, binding)\n  },\n  updated(el: any, binding: DirectiveBinding) {\n    updateMenus(el, binding)\n  },\n} as Directive\n"
  },
  {
    "path": "frontend/src/directives/platform.ts",
    "content": "import { useEnvStore } from '@/stores'\n\nimport type { Directive, DirectiveBinding } from 'vue'\n\nexport default {\n  mounted(el: any, binding: DirectiveBinding) {\n    const envStore = useEnvStore()\n    const supports = binding.value\n    if (!supports.includes(envStore.env.os)) {\n      el.remove()\n    }\n  },\n} as Directive\n"
  },
  {
    "path": "frontend/src/directives/tips.ts",
    "content": "import { type Directive, type DirectiveBinding } from 'vue'\n\nimport { useAppStore } from '@/stores'\nimport { debounce } from '@/utils'\n\nexport default {\n  mounted(el: HTMLElement, binding: DirectiveBinding) {\n    const appStore = useAppStore()\n\n    const delay = binding.modifiers.fast ? 200 : 500\n\n    const show = debounce((x: number, y: number) => {\n      if (el.dataset.showTips === 'true') {\n        appStore.tipsPosition = { x, y }\n        appStore.tipsMessage = binding.value\n        appStore.tipsShow = true\n      }\n    }, delay)\n\n    el.onmouseenter = (e: MouseEvent) => {\n      if (binding.value) {\n        el.dataset.showTips = 'true'\n        show(e.clientX, e.clientY)\n      }\n    }\n\n    el.onmouseleave = () => {\n      appStore.tipsShow = false\n      el.dataset.showTips = 'false'\n    }\n  },\n  beforeUnmount(el: HTMLElement) {\n    const appStore = useAppStore()\n    appStore.tipsShow = false\n    el.dataset.showTips = 'false'\n  },\n} as Directive\n"
  },
  {
    "path": "frontend/src/enums/app.ts",
    "content": "export enum WindowStartState {\n  Normal = 0,\n  Minimised = 2,\n}\n\nexport enum WebviewGpuPolicy {\n  Always = 0,\n  OnDemand = 1,\n  Never = 2,\n}\n\nexport enum Theme {\n  Auto = 'auto',\n  Light = 'light',\n  Dark = 'dark',\n}\n\nexport enum Lang {\n  EN = 'en',\n  ZH = 'zh',\n}\n\nexport enum View {\n  Grid = 'grid',\n  List = 'list',\n}\n\nexport enum ControllerCloseMode {\n  All = 'all',\n  Button = 'button',\n}\n\nexport enum Color {\n  Default = 'default',\n  Green = 'green',\n  Purple = 'purple',\n  Custom = 'custom',\n}\n\nexport enum Branch {\n  Main = 'main',\n  Alpha = 'alpha',\n}\n\nexport enum ScheduledTasksType {\n  UpdateSubscription = 'update::subscription',\n  UpdateRuleset = 'update::ruleset',\n  UpdatePlugin = 'update::plugin',\n  UpdateAllSubscription = 'update::all::subscription',\n  UpdateAllRuleset = 'update::all::ruleset',\n  UpdateAllPlugin = 'update::all::plugin',\n  RunPlugin = 'run::plugin',\n  RunScript = 'run::script',\n}\n\nexport enum PluginTrigger {\n  OnManual = 'on::manual',\n  OnSubscribe = 'on::subscribe',\n  OnGenerate = 'on::generate',\n  OnStartup = 'on::startup',\n  OnShutdown = 'on::shutdown',\n  OnReady = 'on::ready',\n  OnReload = 'on::reload',\n  OnCoreStarted = 'on::core::started',\n  OnCoreStopped = 'on::core::stopped',\n  OnBeforeCoreStart = 'on::before::core::start',\n  OnBeforeCoreStop = 'on::before::core::stop',\n  OnTrayUpdate = 'on::tray::update',\n}\n\nexport enum PluginTriggerEvent {\n  OnInstall = 'onInstall',\n  OnUninstall = 'onUninstall',\n  OnManual = 'onRun',\n  OnTrayUpdate = 'onTrayUpdate',\n  OnSubscribe = 'onSubscribe',\n  OnGenerate = 'onGenerate',\n  OnStartup = 'onStartup',\n  OnShutdown = 'onShutdown',\n  OnReady = 'onReady',\n  OnReload = 'onReload',\n  OnTask = 'onTask',\n  OnConfigure = 'onConfigure',\n  OnCoreStarted = 'onCoreStarted',\n  OnCoreStopped = 'onCoreStopped',\n  OnBeforeCoreStart = 'onBeforeCoreStart',\n  OnBeforeCoreStop = 'onBeforeCoreStop',\n}\n\nexport enum RequestMethod {\n  Get = 'GET',\n  Post = 'POST',\n  Delete = 'DELETE',\n  Put = 'PUT',\n  Head = 'HEAD',\n  Patch = 'PATCH',\n}\n"
  },
  {
    "path": "frontend/src/enums/kernel.ts",
    "content": "export enum LogLevel {\n  Trace = 'trace',\n  Debug = 'debug',\n  Info = 'info',\n  Warn = 'warn',\n  Error = 'error',\n  Fatal = 'fatal',\n  Panic = 'panic',\n}\n\nexport enum ClashMode {\n  Global = 'global',\n  Rule = 'rule',\n  Direct = 'direct',\n}\n\nexport enum Inbound {\n  Mixed = 'mixed',\n  Socks = 'socks',\n  Http = 'http',\n  Tun = 'tun',\n}\n\nexport enum Outbound {\n  Direct = 'direct',\n  Block = 'block',\n  Selector = 'selector',\n  Urltest = 'urltest',\n}\n\nexport enum TunStack {\n  System = 'system',\n  GVisor = 'gvisor',\n  Mixed = 'mixed',\n}\n\nexport enum RulesetType {\n  Inline = 'inline',\n  Local = 'local',\n  Remote = 'remote',\n}\n\nexport enum RulesetFormat {\n  Source = 'source',\n  Binary = 'binary',\n}\n\nexport enum RuleType {\n  Inbound = 'inbound',\n  Network = 'network',\n  Protocol = 'protocol',\n  Domain = 'domain',\n  DomainSuffix = 'domain_suffix',\n  DomainKeyword = 'domain_keyword',\n  DomainRegex = 'domain_regex',\n  SourceIPCidr = 'source_ip_cidr',\n  IPCidr = 'ip_cidr',\n  IpIsPrivate = 'ip_is_private',\n  SourcePort = 'source_port',\n  SourcePortRange = 'source_port_range',\n  Port = 'port',\n  PortRange = 'port_range',\n  ProcessName = 'process_name',\n  ProcessPath = 'process_path',\n  ProcessPathRegex = 'process_path_regex',\n  ClashMode = 'clash_mode',\n  RuleSet = 'rule_set',\n  IpAcceptAny = 'ip_accept_any',\n  // GUI\n  Inline = 'inline',\n  InsertionPoint = 'InsertionPoint',\n}\n\nexport enum Strategy {\n  Default = 'default',\n  PreferIPv4 = 'prefer_ipv4',\n  PreferIPv6 = 'prefer_ipv6',\n  IPv4Only = 'ipv4_only',\n  IPv6Only = 'ipv6_only',\n}\n\nexport enum DnsServer {\n  Local = 'local',\n  Hosts = 'hosts',\n  Tcp = 'tcp',\n  Udp = 'udp',\n  Tls = 'tls',\n  Https = 'https',\n  Quic = 'quic',\n  H3 = 'h3',\n  Dhcp = 'dhcp',\n  FakeIP = 'fakeip',\n}\n\nexport enum RuleAction {\n  Route = 'route',\n  RouteOptions = 'route-options',\n  Reject = 'reject',\n  HijackDNS = 'hijack-dns',\n  Sniff = 'sniff',\n  Resolve = 'resolve',\n  Predefined = 'predefined',\n}\n\nexport enum RuleActionReject {\n  Default = 'default',\n  Drop = 'drop',\n  Reply = 'reply',\n}\n\nexport enum Sniffer {\n  Http = 'http',\n  Tls = 'tls',\n  Quic = 'quic',\n  Stun = 'stun',\n  Dns = 'dns',\n  Bittorrent = 'bittorrent',\n  Dtls = 'dtls',\n  Ssh = 'ssh',\n  Rdp = 'rdp',\n  Ntp = 'ntp',\n}\n"
  },
  {
    "path": "frontend/src/hooks/index.ts",
    "content": "export * from './useBool'\n"
  },
  {
    "path": "frontend/src/hooks/useBool.ts",
    "content": "import { ref } from 'vue'\n\nexport const useBool = (initialValue: boolean) => {\n  const value = ref(initialValue)\n\n  const toggle = () => {\n    value.value = !value.value\n  }\n\n  return [value, toggle] as const\n}\n"
  },
  {
    "path": "frontend/src/hooks/useCoreBranch.ts",
    "content": "import { computed, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport {\n  Download,\n  HttpCancel,\n  UnzipZIPFile,\n  UnzipTarGZFile,\n  HttpGet,\n  Exec,\n  MoveFile,\n  RemoveFile,\n  AbsolutePath,\n  BrowserOpenURL,\n  MakeDir,\n  FileExists,\n  OpenDir,\n} from '@/bridge'\nimport { CoreWorkingDirectory } from '@/constant/kernel'\nimport { Branch } from '@/enums/app'\nimport { useAppSettingsStore, useEnvStore, useKernelApiStore } from '@/stores'\nimport {\n  getGitHubApiAuthorization,\n  GrantTUNPermission,\n  ignoredError,\n  confirm,\n  message,\n  debounce,\n  getKernelFileName,\n  getKernelAssetFileName,\n} from '@/utils'\n\nconst StableUrl = 'https://api.github.com/repos/SagerNet/sing-box/releases/latest'\nconst AlphaUrl = 'https://api.github.com/repos/SagerNet/sing-box/releases?per_page=3'\n\nconst StablePage = 'https://github.com/SagerNet/sing-box/releases/latest'\nconst AlphaPage = 'https://github.com/SagerNet/sing-box/releases'\n\nexport const useCoreBranch = (isAlpha = false) => {\n  const releaseUrl = isAlpha ? AlphaUrl : StableUrl\n\n  const localVersion = ref('')\n  const remoteVersion = ref('')\n  const versionDetail = ref('')\n\n  const localVersionLoading = ref(false)\n  const remoteVersionLoading = ref(false)\n  const downloading = ref(false)\n  const downloadCompleted = ref(false)\n\n  const rollbackable = ref(false)\n\n  const { t } = useI18n()\n  const envStore = useEnvStore()\n  const appSettings = useAppSettingsStore()\n  const kernelApiStore = useKernelApiStore()\n\n  const restartable = computed(() => {\n    const { branch } = appSettings.app.kernel\n    if (!kernelApiStore.running) return false\n    return localVersion.value && downloadCompleted.value && (branch === Branch.Alpha) === isAlpha\n  })\n\n  const updatable = computed(\n    () => remoteVersion.value && localVersion.value !== remoteVersion.value,\n  )\n\n  const grantable = computed(() => localVersion.value && envStore.env.os !== 'windows')\n\n  const CoreFilePath = `${CoreWorkingDirectory}/${getKernelFileName(isAlpha)}`\n  const CoreBakFilePath = `${CoreFilePath}.bak`\n\n  const downloadCore = async () => {\n    downloading.value = true\n    try {\n      const { body } = await HttpGet<Record<string, any>>(releaseUrl, {\n        Authorization: getGitHubApiAuthorization(),\n      })\n      if (body.message) throw body.message\n\n      const release = isAlpha ? body.find((v: any) => v.prerelease === true) : body\n      if (!release) throw 'Not Found'\n      const { assets, tag_name } = release\n      const assetName = getKernelAssetFileName(tag_name.replace('v', ''))\n      const asset = assets.find((v: any) => v.name === assetName)\n      if (!asset) throw 'Asset Not Found:' + assetName\n      if (asset.uploader.type !== 'Bot') {\n        await confirm('common.warning', 'settings.kernel.risk', {\n          type: 'text',\n          okText: 'settings.kernel.stillDownload',\n        })\n      }\n\n      const downloadCacheFile = `data/.cache/${assetName}`\n      const downloadCancelId = downloadCacheFile\n\n      const { update, destroy } = message.info('common.downloading', 10 * 60 * 1_000, () => {\n        HttpCancel(downloadCancelId)\n        setTimeout(() => RemoveFile(downloadCacheFile), 1000)\n      })\n\n      await MakeDir(CoreWorkingDirectory)\n\n      await Download(\n        asset.browser_download_url,\n        downloadCacheFile,\n        undefined,\n        (progress, total) => {\n          update(t('common.downloading') + ((progress / total) * 100).toFixed(2) + '%')\n        },\n        { CancelId: downloadCancelId },\n      ).finally(destroy)\n\n      const stableFileName = getKernelFileName()\n\n      await ignoredError(MoveFile, CoreFilePath, CoreBakFilePath)\n\n      if (assetName.endsWith('.zip')) {\n        await UnzipZIPFile(downloadCacheFile, 'data/.cache')\n        const tmpPath = `data/.cache/${assetName.replace('.zip', '')}`\n        await MoveFile(`${tmpPath}/${stableFileName}`, CoreFilePath)\n        await RemoveFile(tmpPath)\n      } else if (assetName.endsWith('.tar.gz')) {\n        await UnzipTarGZFile(downloadCacheFile, 'data/.cache')\n        const tmpPath = `data/.cache/${assetName.replace('.tar.gz', '')}`\n        await MoveFile(`${tmpPath}/${stableFileName}`, CoreFilePath)\n        await RemoveFile(tmpPath)\n      }\n\n      await RemoveFile(downloadCacheFile)\n\n      if (!CoreFilePath.endsWith('.exe')) {\n        await ignoredError(Exec, 'chmod', ['+x', await AbsolutePath(CoreFilePath)])\n      }\n\n      refreshLocalVersion()\n      downloadCompleted.value = true\n      message.success('common.success')\n    } catch (error: any) {\n      console.log(error)\n      message.error(error.message || error)\n      downloadCompleted.value = false\n    }\n    downloading.value = false\n  }\n\n  const getLocalVersion = async (showTips = false) => {\n    localVersionLoading.value = true\n    try {\n      const res = await Exec(CoreFilePath, ['version'])\n      versionDetail.value = res.trim()\n      return res.match(/version (\\S+)/)?.[1] || ''\n    } catch (error: any) {\n      console.log(error)\n      showTips && message.error(error)\n    } finally {\n      localVersionLoading.value = false\n    }\n    return ''\n  }\n\n  const getRemoteVersion = async (showTips = false) => {\n    remoteVersionLoading.value = true\n    try {\n      const { body } = await HttpGet<Record<string, any>>(releaseUrl, {\n        Authorization: getGitHubApiAuthorization(),\n      })\n      const release = isAlpha ? body.find((v: any) => v.prerelease === true) : body\n      if (!release) throw 'Not Found'\n      const { tag_name } = release\n      return tag_name.replace('v', '') as string\n    } catch (error: any) {\n      console.log(error)\n      showTips && message.error(error)\n    } finally {\n      remoteVersionLoading.value = false\n    }\n    return ''\n  }\n\n  const restartCore = async () => {\n    if (!kernelApiStore.running) return\n    try {\n      await kernelApiStore.restartCore()\n      downloadCompleted.value = false\n      message.success('common.success')\n    } catch (error: any) {\n      message.error(error)\n    }\n  }\n\n  const refreshLocalVersion = async (showTips = false) => {\n    localVersion.value = await getLocalVersion(showTips)\n  }\n\n  const refreshRemoteVersion = async (showTips = false) => {\n    remoteVersion.value = await getRemoteVersion(showTips)\n  }\n\n  const grantCorePermission = async () => {\n    await GrantTUNPermission(CoreFilePath)\n    message.success('common.success')\n  }\n\n  const rollbackCore = async () => {\n    await confirm('common.warning', 'settings.kernel.rollback')\n\n    const doRollback = () => MoveFile(CoreBakFilePath, CoreFilePath)\n\n    const { branch } = appSettings.app.kernel\n    const isCurrentRunning = kernelApiStore.running && (branch === Branch.Alpha) === isAlpha\n    if (isCurrentRunning) {\n      await kernelApiStore.restartCore(doRollback)\n    } else {\n      await doRollback()\n    }\n    refreshLocalVersion()\n    message.success('common.success')\n  }\n\n  const openReleasePage = () => {\n    BrowserOpenURL(isAlpha ? AlphaPage : StablePage)\n  }\n\n  const openFileLocation = async () => {\n    await OpenDir(CoreWorkingDirectory)\n  }\n\n  watch(\n    () => appSettings.app.kernel.branch,\n    () => (downloadCompleted.value = false),\n  )\n\n  watch(\n    [localVersion, downloadCompleted],\n    debounce(async () => {\n      rollbackable.value = await FileExists(CoreBakFilePath)\n    }, 500),\n  )\n\n  refreshLocalVersion()\n  refreshRemoteVersion()\n\n  return {\n    restartable,\n    updatable,\n    grantable,\n    rollbackable,\n    versionDetail,\n    localVersion,\n    localVersionLoading,\n    remoteVersion,\n    remoteVersionLoading,\n    downloading,\n    refreshLocalVersion,\n    refreshRemoteVersion,\n    downloadCore,\n    restartCore,\n    rollbackCore,\n    grantCorePermission,\n    openReleasePage,\n    openFileLocation,\n  }\n}\n"
  },
  {
    "path": "frontend/src/lang/index.ts",
    "content": "import { createI18n } from 'vue-i18n'\n\nimport { ReadFile } from '@/bridge'\nimport { LocalesFilePath } from '@/constant/app'\nimport { Lang } from '@/enums/app'\n\nconst i18n = createI18n({\n  legacy: false,\n  locale: 'en',\n  fallbackWarn: false,\n  missingWarn: false,\n  messages: {},\n})\n\nexport const loadLocale = async (locale = i18n.global.locale.value) => {\n  if ([Lang.ZH, Lang.EN].includes(locale as Lang)) {\n    const message = await import(`./locale/${locale}.ts`)\n    i18n.global.setLocaleMessage(locale, message.default)\n  } else {\n    const message = await ReadFile(`${LocalesFilePath}/${locale}.json`).catch(() => '')\n    message && i18n.global.setLocaleMessage(locale, JSON.parse(message))\n  }\n}\n\nexport default i18n\n"
  },
  {
    "path": "frontend/src/lang/locale/en.ts",
    "content": "export default {\n  common: {\n    grid: 'Grid',\n    list: 'List',\n    add: 'Add',\n    added: 'Added',\n    more: 'More',\n    edit: 'Edit',\n    clear: 'Clear',\n    update: 'Update',\n    delete: 'Delete',\n    cancel: 'Cancel',\n    save: 'Save',\n    nextStep: 'Next',\n    prevStep: 'Back',\n    disabled: 'Disabled',\n    enabled: 'Enabled',\n    preview: 'Preview',\n    warning: 'Warning',\n    disable: 'Disable',\n    enable: 'Enable',\n    use: 'Use',\n    none: 'none',\n    close: 'Close',\n    reset: 'Reset',\n    pause: 'Pause',\n    resume: 'Resume',\n    details: 'Details',\n    updateAll: 'Update All',\n    updateTime: 'Update Time',\n    keywords: 'Keywords',\n    success: 'Success',\n    copy: 'Copy',\n    copied: 'Copied',\n    auto: 'Auto',\n    import: 'Import',\n    install: 'Install',\n    uninstall: 'Uninstall',\n    run: 'Run',\n    refresh: 'Refresh',\n    confirm: 'OK',\n    selectAll: 'Select All',\n    http: 'Remote',\n    file: 'Local',\n    openFile: 'Open File',\n    develop: 'Develop',\n    canceled: 'Canceled',\n    downloading: 'Downloading...',\n    empty: 'Data is empty',\n    pressAgainToClose: 'Press again to close the modal',\n  },\n  kernel: {\n    rule: 'Rule',\n    global: 'Global',\n    direct: 'Direct',\n    ruleDesc: 'Route traffic based on rules',\n    globalDesc: 'Only follow the Global group',\n    directDesc: 'Directly connect all traffic',\n    log: {\n      disabled: 'Disabled',\n      level: 'Level',\n      output: 'Output',\n      timestamp: 'Timestamp',\n      trace: 'trace',\n      debug: 'debug',\n      info: 'info',\n      warn: 'warn',\n      error: 'error',\n      fatal: 'fatal',\n      panic: 'panic',\n    },\n    clash_api: {\n      external_controller: 'External Controller',\n      external_ui: 'External UI',\n      external_ui_download_url: 'Web UI Download URL',\n      external_ui_download_detour: 'Web UI Download Detour',\n      secret: 'RESTful API Secret',\n      default_mode: 'Mode',\n      access_control_allow_origin: 'CORS allowed origins',\n      access_control_allow_private_network: 'Allow access from private network',\n    },\n    cache_file: {\n      enabled: 'Enabled',\n      path: 'Path to the cache file',\n      cache_id: 'Identifier in the cache file',\n      store_fakeip: 'Store Fake-IP',\n      store_rdrc: 'Store Rejected DNS Response',\n      rdrc_timeout: 'Timeout of rejected DNS response cache',\n    },\n    inbounds: {\n      enable: 'Enable',\n      tag: 'Tag',\n      users: 'Http/Socks users',\n      listen: {\n        listen: 'Listen',\n        listen_port: 'Port',\n        tcp_fast_open: 'TCP Fast Open',\n        tcp_multi_path: 'TCP Multi Path',\n        udp_fragment: 'UDP Fragmentation',\n      },\n      tun: {\n        interface_name: 'Interface Name',\n        address: 'IPv4 & IPv6 Prefix',\n        mtu: 'MTU',\n        auto_route: 'Auto Route',\n        strict_route: 'Strict Route',\n        route_address: 'Route Address',\n        route_exclude_address: 'Route Exclude Address',\n        endpoint_independent_nat: 'Endpoint Independent NAT',\n        stack: 'Stack',\n        system: 'System',\n        gvisor: 'gVisor',\n        mixed: 'Mixed',\n      },\n      mixedPort: 'Mixed Port',\n      httpPort: 'HTTP(s) Port',\n      socksPort: 'SOCKS5 Port',\n    },\n    outbounds: {\n      name: 'Outbound',\n      tag: 'Tag',\n      type: 'Type',\n      url: 'URL',\n      interval: 'Interval(min)',\n      tolerance: 'Tolerance(ms)',\n      interrupt_exist_connections: 'Interrupt Exist Connections',\n      direct: 'Direct',\n      block: 'Block',\n      directDesc: 'No settings',\n      selector: 'Selector',\n      urltest: 'URLTest',\n      notFound: 'Some outbound tags or proxies are missing; please clean them up.',\n      needToAdd: 'At least reference one outbound tag or proxy.',\n      refsSubscription: 'Reference subscription',\n      refsOutbound: 'Reference outbound',\n      sort: 'View and Sort',\n      refs: 'Reference subscription & outbound',\n      noSubs: 'The subscription list is empty.',\n      empty: 'No available proxies under this subscription.',\n      builtIn: 'Built-In',\n      subscriptions: 'Subscriptions',\n      include: 'Include',\n      exclude: 'Exclude',\n    },\n    route: {\n      tab: {\n        common: 'Common',\n        rules: 'Rules',\n        rule_set: 'Rule-Set',\n      },\n      find_process: 'Find Process',\n      auto_detect_interface: 'Auto Detect Interface',\n      default_interface: 'Default Interface',\n      final: 'Final Outbound Tag',\n      default_domain_resolver: {\n        server: 'Default Domain Resolver',\n        client_subnet: 'Client Subnet',\n      },\n      rule_set: {\n        type: {\n          name: 'Name',\n          inline: 'Inline',\n          local: 'Local',\n          remote: 'Remote',\n        },\n        tag: 'Tag',\n        format: {\n          name: 'Format',\n          binary: 'Binary',\n          source: 'Source',\n        },\n        url: 'URL',\n        download_detour: 'Download Detour',\n        update_interval: 'Update Interval',\n        path: 'Path',\n        notFound: 'The rule set has been lost.',\n        empty: 'The rule set list is empty.',\n      },\n      rules: {\n        type: 'Type',\n        action: {\n          name: 'Action',\n          route: 'Route',\n          'route-options': 'Route-Options',\n          reject: 'Reject',\n          predefined: 'Predefined',\n          'hijack-dns': 'Hijack-DNS',\n          sniff: 'Sniff',\n          resolve: 'Resolve DNS',\n          rejectMethod: 'Method',\n          rejectDefault: 'default',\n          rejectDrop: 'drop',\n          rejectReply: 'reply',\n        },\n        outbound: 'Outbound Tag',\n        routeOptions: 'Route Options',\n        sniffer: {\n          name: 'Sniffer',\n          http: 'http',\n          tls: 'tls',\n          quic: 'quic',\n          stun: 'stun',\n          dns: 'dns',\n          bittorrent: 'bittorrent',\n          dtls: 'dtls',\n          ssh: 'ssh',\n          rdp: 'rdp',\n          ntp: 'ntp',\n        },\n        server: 'DNS Server',\n        payload: 'Payload',\n        strategy: 'Strategy',\n        disable_cache: 'Disable Cache',\n        client_subnet: 'Client Subnet',\n        notFound: 'Outbound tag is missing.',\n        invalid: 'Invalid Parameter',\n        invert: 'Invert',\n      },\n    },\n    rules: {\n      type: {\n        name: 'Type',\n        inbound: 'inbound',\n        network: 'network',\n        protocol: 'protocol',\n        domain: 'domain',\n        domain_suffix: 'domain_suffix',\n        domain_keyword: 'domain_keyword',\n        domain_regex: 'domain_regex',\n        source_ip_cidr: 'source_ip_cidr',\n        ip_cidr: 'ip_cidr',\n        ip_is_private: 'ip_is_private',\n        source_port: 'source_port',\n        source_port_range: 'source_port_range',\n        port: 'port',\n        port_range: 'port_range',\n        process_name: 'process_name',\n        process_path: 'process_path',\n        process_path_regex: 'process_path_regex',\n        clash_mode: 'clash_mode',\n        rule_set: 'rule_set',\n        ip_accept_any: 'ip_accept_any',\n        inline: 'Inline',\n      },\n    },\n    strategy: {\n      name: 'Strategy',\n      default: 'Default',\n      byDnsRules: 'Determined by DNS rules',\n      prefer_ipv4: 'Prefer IPV4',\n      prefer_ipv6: 'Prefer IPV6',\n      ipv4_only: 'IPV4 Only',\n      ipv6_only: 'IPV6 Only',\n    },\n    dns: {\n      tab: {\n        common: 'Common',\n        servers: 'Servers',\n        rules: 'Rules',\n      },\n      tag: 'Tag',\n      type: {\n        name: 'Type',\n        local: 'Local',\n        hosts: 'Hosts',\n        tcp: 'TCP',\n        udp: 'UDP',\n        tls: 'TLS',\n        https: 'HTTPS',\n        quic: 'QUIC',\n        h3: 'HTTP/3',\n        predefined: 'Predefined',\n        dhcp: 'DHCP',\n        fakeip: 'Fake-IP',\n      },\n      detour: 'Detour',\n      server: 'Server',\n      server_port: 'Server Port',\n      domain_resolver: 'Domain Resolver',\n      path: 'Path',\n      interface: 'Interface',\n      disable_cache: 'Disable Cache',\n      client_subnet: 'Client Subnet',\n      disable_expire: 'Disable Expire',\n      independent_cache: 'Independent Cache',\n      final: 'Final DNS',\n      strategy: 'Strategy',\n      inet4_range: 'Fake-IP Range(IPv4)',\n      inet6_range: 'Fake-IP Range(IPv6)',\n      hosts_path: 'Hosts Path',\n      predefined: 'Predefined',\n      rules: {\n        type: 'Type',\n        payload: 'Payload',\n        action: 'Action',\n        server: 'Server',\n      },\n    },\n    mode: 'Mode',\n    'allow-lan': 'Allow LAN',\n    'disallow-lan': 'Disallow LAN',\n    notFound: 'Core Not Found',\n    insertionPoint: 'The new rule will be inserted here',\n    addInsertionPoint: 'Add insertion point',\n  },\n  router: {\n    overview: 'Overview',\n    subscriptions: 'Subscriptions',\n    rulesets: 'Rulesets',\n    plugins: 'Plugins',\n    settings: 'Settings',\n    about: 'About',\n    profiles: 'Profiles',\n    kernel: 'Core',\n    scheduledtasks: 'Tasks',\n  },\n  home: {\n    mode: 'Proxy Mode',\n    global: 'Global',\n    rule: 'Rule',\n    direct: 'Direct',\n    quickStart: 'Quick Start',\n    noProfile: 'Welcome to the {0}, click the button to get started.',\n    initSuccessful: 'Initialization successful',\n    overview: {\n      expandAll: 'Expand All',\n      collapseAll: 'Collapse All',\n      refresh: 'Refresh',\n      delayTest: 'Delay Test',\n      stop: 'Stop Core',\n      restart: 'Restart Core',\n      viewlog: 'view log',\n      start: 'Click to Start',\n      noLogs: 'Log is empty',\n      systemProxy: 'System Proxy',\n      tunMode: 'TUN Mode',\n      traffic: 'Traffic',\n      realtimeTraffic: 'Real-time Traffic',\n      totalTraffic: 'Total Traffic',\n      connections: 'Connections',\n      memory: 'Memory',\n      transmit: 'Transmit',\n      receive: 'Receive',\n      settings: 'Core Settings',\n      settingsTips:\n        'Takes effect temporarily. For persistent changes, please modify the `profile` settings.',\n      updateGEO: 'Update GEO',\n      needPort: 'Please add a Mixed/Http/Socks inbound first',\n      needTun: 'Please add a TUN inbound first',\n    },\n    controller: {\n      name: 'Controller',\n      autoClose: 'Auto-close',\n      unAvailable: 'Show UnAvailable',\n      cardMode: 'Card Mode',\n      sortBy: 'Sort By Delay',\n      delay: 'Latency test URL',\n      timeout: 'Latency test timeout (ms)',\n      concurrencyLimit: 'Latency test concurrency',\n      cardColumns: 'Number of card columns',\n      sensitivity: 'Controller Scroll Sensitivity',\n      closeMode: {\n        name: 'Controller Close Mode',\n        all: 'Scroll or Button',\n        button: 'Button Only',\n      },\n    },\n    connections: {\n      type: 'Type',\n      processPath: 'Process Path',\n      sourceIP: 'Source',\n      destinationIP: 'Destination IP',\n      host: 'Host',\n      inbound: 'Inbound',\n      rule: 'Rule',\n      chains: 'Chains',\n      upload: 'Upload',\n      download: 'Download',\n      uploadSpeed: 'UL Speed',\n      downSpeed: 'DL Speed',\n      time: 'Time',\n      close: 'Close',\n      addToDirect: 'Add To DIRECT',\n      addToProxy: 'Add To PROXY',\n      addToReject: 'Add To REJECT',\n      active: 'Active',\n      closed: 'Closed',\n      closeAll: 'Close all connections',\n      sort: 'Sorting and Setting Visibility',\n      details: 'Connection Details',\n    },\n  },\n  subscribe: {\n    manual: 'MANUAL',\n    name: 'Name',\n    url: 'Remote Url',\n    localPath: 'Local Path',\n    website: 'Website',\n    path: 'Save Path',\n    include: 'Include Keywords',\n    exclude: 'Exclude Keywords',\n    includeProtocol: 'Include Protocol',\n    excludeProtocol: 'Exclude Protocol',\n    proxyPrefix: 'Proxy Prefix',\n    updating: 'Updating',\n    useragent: 'User-Agent',\n    inSecure: 'Skip TLS Verification',\n    requestMethod: 'Request Method',\n    requestTimeout: 'Request Timeout (seconds)',\n    header: {\n      request: 'Request Header',\n      response: 'Response Header',\n    },\n  },\n  subscribes: {\n    download: 'Download',\n    upload: 'Upload',\n    total: 'Total',\n    expire: 'Expire',\n    subtype: 'Subscription Type',\n    website: 'Website',\n    empty: 'The subscription list is empty. Please{action}a subscription first.',\n    enterLink: 'Enter subscription link',\n    proxyCount: 'Proxy Count',\n    editProxies: 'Edit Proxies',\n    editSourceFile: 'Edit Proxies(Source)',\n    copySub: 'Copy Link',\n    script: 'Script',\n    proxies: {\n      type: 'Protocol',\n      name: 'Name',\n      add: 'Add Proxy',\n    },\n  },\n  profile: {\n    name: 'Name',\n    generalSettings: 'General Settings',\n    advancedSettings: 'Advanced Settings',\n    step: {\n      name: 'Name',\n      general: 'General',\n      inbounds: 'Inbounds',\n      outbounds: 'Outbounds',\n      route: 'Route',\n      dns: 'DNS',\n      'mixin-script': 'Mixin & Script',\n    },\n    proxies: 'Reference proxies',\n    use: 'Reference subscriptions',\n    noSubs: 'There are no available subscriptions.',\n    group: 'Group Details',\n    rule: 'Rule Details',\n    auto: 'This configuration is managed by your subscription and will be overwritten upon update!\\nUse the plugin system to make permanent changes.',\n    mixinSettings: {\n      name: 'Mixin',\n      priority: 'Priority',\n      format: 'Format',\n      mixin: 'Mixin',\n      gui: 'GUI',\n    },\n    scriptSettings: {\n      name: 'Script',\n    },\n  },\n  profiles: {\n    shouldStop: 'Unable to delete, this profile is in use.',\n    empty: 'The profiles list is empty, Please{action}a profile first.',\n    copytoClipboard: 'Generate config to clipboard',\n    generateAndView: 'Generate and View',\n    copy: 'Copy and Paste',\n    start: 'Start/Restart with this Profile',\n    inbounds: 'Inbounds',\n    outbounds: 'Outbounds',\n    dnsServers: 'DNS Servers',\n    dnsRules: 'DNS Rules',\n  },\n  ruleset: {\n    manual: 'MANUAL',\n    format: {\n      name: 'Format',\n      source: 'Source',\n      binary: 'Binary',\n    },\n    rulesetType: 'Ruleset Type',\n    name: 'Name',\n    url: 'Remote Url',\n    path: 'Save Path',\n    interval: 'Interval',\n    updating: 'Updating',\n  },\n  rulesets: {\n    hub: 'Ruleset-Hub',\n    total: 'Number of rule-sets',\n    noDesc: 'No description',\n    updating: 'Updating',\n    updateSuccess: 'Ruleset-Hub updated successfully',\n    fetching: 'fetching...',\n    empty: 'The ruleset list is empty. Please{action}or import from the{import}first.',\n    rulesetCount: 'Ruleset Count',\n    editRuleset: 'Edit Rules',\n    selectRuleType: 'Select Rule Type',\n  },\n  plugin: {\n    trigger: 'Trigger',\n    'on::manual': 'on::manual',\n    'on::startup': 'on::startup',\n    'on::shutdown': 'on::shutdown',\n    'on::generate': 'on::generate',\n    'on::subscribe': 'on::subscribe',\n    'on::ready': 'on::ready',\n    'on::reload': 'on::reload',\n    'on::task': 'on::task',\n    'on::install': 'on::install',\n    'on::uninstall': 'on::uninstall',\n    'on::configure': 'on::configure',\n    'on::core::started': 'on::core::started',\n    'on::core::stopped': 'on::core::stopped',\n    'on::before::core::start': 'on::before::core::start',\n    'on::before::core::stop': 'on::before::core::stop',\n    'on::tray::update': 'on::tray::update',\n    name: 'Name',\n    version: 'Version',\n    description: 'Description',\n    url: 'Remote Url',\n    install: 'Installation required',\n    installed: 'Installed',\n    path: 'Save Path',\n    type: 'Type',\n    menus: 'Menus',\n    hasUI: 'Has user interface',\n    context: 'Context',\n    configuration: ' Configuration',\n    menuKey: 'Menu Title',\n    menuValue: 'Trigger function name',\n    selectComponent: 'Select a component',\n    confName: 'Name',\n    confDescription: 'Description',\n    confKey: 'Key',\n    confDefault: 'Default',\n    options: 'Options',\n    restore: 'Reset to default',\n  },\n  plugins: {\n    updating: 'Updating',\n    empty: 'The plugin list is empty. Please{action}or import from the{import}first.',\n    source: 'Source',\n    reload: 'Reload',\n    configuration: 'Configure',\n    hub: 'Plugin-Hub',\n    update: 'Update List',\n    checkForUpdates: 'Check for updates',\n    updateSuccess: 'Plugin-Hub updated successfully',\n    total: 'Number of plug-ins',\n    removeConfiguration: 'Do you want to remove the plugin configuration?',\n    testRun: 'TestRun',\n    deprecated: 'Deprecated',\n    newVersion: 'New',\n  },\n  scheduledtask: {\n    name: 'Name',\n    type: 'Type',\n    script: 'Script',\n    subscriptions: 'Subscriptions',\n    rulesets: 'Rulesets',\n    plugins: 'Plugins',\n    cron: 'Cron',\n    notification: 'Task Completed Notification',\n    cronTips: 'Seconds Minutes Hours \"Day of month\" Month \"Day of week\"',\n    lastTime: 'Last Time',\n    'update::subscription': 'update::subscription',\n    'update::ruleset': 'update::ruleset',\n    'update::plugin': 'update::plugin',\n    'update::all::subscription': 'update::all::subscription',\n    'update::all::ruleset': 'update::all::ruleset',\n    'update::all::plugin': 'update::all::plugin',\n    'run::plugin': 'run::plugin',\n    'run::script': 'run::script',\n  },\n  scheduledtasks: {\n    logs: 'Logs',\n    name: 'Plugin',\n    duration: 'Duration',\n    startTime: 'Start Time',\n    endTime: 'End Time',\n    time: 'Time',\n    result: 'Result',\n    empty: 'The scheduled task list is empty. Please{action}a scheduled task first.',\n    run: 'Run now',\n    log: 'View log',\n    next: 'Next Run Time',\n  },\n  settings: {\n    personalization: 'Personalization',\n    behavior: 'Behavior',\n    systemProxy: 'System Proxy',\n    advanced: 'Advanced',\n    features: 'Features',\n    general: 'General',\n    theme: {\n      name: 'Theme',\n      light: 'Light Mode',\n      dark: 'Dark Mode',\n      auto: 'System',\n    },\n    color: {\n      name: 'Color',\n      default: 'Default',\n      green: 'Green',\n      purple: 'Purple',\n      custom: 'Custom',\n      primary: 'Primary',\n      secondary: 'Secondary',\n    },\n    fontFamily: 'Font-Family',\n    resetFont: 'Reset Font-Family',\n    appFolder: {\n      name: 'App Folder',\n      open: 'Open application folder',\n    },\n    lang: {\n      name: 'Language',\n      load: 'Load language files',\n      zh: '简体中文',\n      en: 'English',\n    },\n    pages: {\n      name: 'Page visibility',\n    },\n    windowState: {\n      normal: 'Normal window',\n      maximised: 'Maximised',\n      minimised: 'Minimize window',\n      fullscreen: 'Fullscreen',\n    },\n    webviewGpuPolicy: {\n      name: 'Webview Gpu Policy',\n      always: 'Always',\n      onDemand: 'OnDemand',\n      never: 'Never',\n    },\n    needRestart: 'Restart Required',\n    needAdmin: 'Admin required',\n    exitOnClose: 'Exit on window close',\n    closeKernelOnExit: 'Stop core on exit',\n    autoSetSystemProxy: 'Auto-configure System Proxy',\n    proxyBypassList: 'Proxy Bypass List',\n    proxyBypassListTips: 'Separate with semicolons',\n    autoStartKernel: 'Start core on launch',\n    realMemoryUsage: 'Show actual core memory usage',\n    autoRestartKernel: {\n      name: 'Auto-restart core on config changes',\n      tips: 'It will interrupt all connections and may fail to restart',\n    },\n    admin: 'Run as admin',\n    addPluginToMenu: 'Add plugin to tray menu',\n    addGroupToMenu: 'Add proxy group to tray menu',\n    multipleInstance: 'Allow multiple app instances',\n    rollingRelease: 'Enable Rolling Release',\n    debugOutline: 'Show component outlines',\n    debugNoAnimation: 'Disable animations',\n    debugNoRounded: 'Disable rounded corners',\n    debugBorder: 'Show window border',\n    startup: {\n      name: 'Run at startup',\n      delay: 'Delay(s)',\n      startupDelay: 'Startup delay',\n    },\n    kernel: {\n      name: 'sing-box',\n      version: 'Switch version',\n      stable: 'Stable version',\n      alpha: 'Alpha version',\n      grant: 'Grant Privileges',\n      openTip: 'Open File Location',\n      linkTip: 'View on GitHub Releases',\n      local: 'Local',\n      remote: 'Remote',\n      update: 'Update',\n      restart: 'Restart Core',\n      risk: 'This version is not auto-built by GitHub and may pose a security risk.',\n      stillDownload: 'Still download',\n      rollbackTip: 'Rollback to the previous version',\n      rollback: 'Are you sure you want to roll back to the previous version?',\n      clearCache: 'Clear Cache',\n      config: {\n        name: 'Runtime Configuration',\n        env: 'Environment Variables',\n        args: 'Runtime Arguments',\n      },\n    },\n    plugin: {\n      resetSetting: 'Reset setting',\n      resetSettings: 'Reset all settings',\n    },\n    userAgent: {\n      name: 'User-Agent',\n      reset: 'Reset User-Agent',\n      tips: 'Used for this app’s network requests',\n    },\n    githubapi: {\n      name: 'GitHub REST API Token',\n      tips: 'Provides a higher rate limit',\n    },\n  },\n  about: {\n    new: 'New',\n    restart: 'Restart APP',\n    noDownloadLink: 'No download link found',\n    updateSuccessfulRestart: 'Update completed, please restart the App',\n    updateSuccessfulReplace: 'Download completed, please manually replace the App',\n    updateSuccessful: 'Update completed',\n    newVersion: 'New version found',\n    latestVersion: 'Already the latest version',\n  },\n  titlebar: {\n    resetSize: 'Reset Window',\n    reload: 'Reload Window',\n    restart: 'Restart App',\n    exitApp: 'Exit App',\n    exitPending: 'Waiting for the program to exit...',\n    exitTimeout: 'The program exit timed out. Do you want to force quit?',\n    exitError: 'An error occurred during exit. Do you want to force quit?\\n\\nReason: {reason}',\n    reloadPending: 'Waiting for the program to reload...',\n    reloadTimeout: 'The reload timed out. Do you want to force reload?',\n    reloadError:\n      'An error occurred during reload. Do you want to force reload?\\n\\nReason: {reason}',\n  },\n  outbound: {\n    select: '🚀 Select',\n    urltest: '🎈 Auto',\n    direct: '🎯 Direct',\n    block: '🛑 Block',\n    fallback: '🐟 Fallback',\n  },\n  tray: {\n    showMainWindow: 'Show Main Window',\n    restart: 'Restart',\n    restartTip: 'Restart App',\n    exit: 'Exit',\n    exitTip: 'Exit App',\n    proxyGroup: 'Proxy Group',\n    setSystemProxy: 'Set System Proxy',\n    clearSystemProxy: 'Clear System Proxy',\n    tun: 'Tun Mode',\n    enableTunMode: 'Enable Tun Mode',\n    disableTunMode: 'Disable Tun Mode',\n    kernel: 'Core',\n    proxy: 'System Proxy',\n    startKernel: 'Start Core',\n    stopKernel: 'Stop Core',\n    restartKernel: 'Restart Core',\n    plugins: 'Plugins',\n  },\n  commands: {\n    noMatching: 'No matching commands',\n  },\n}\n"
  },
  {
    "path": "frontend/src/lang/locale/zh.ts",
    "content": "export default {\n  common: {\n    grid: '网格',\n    list: '列表',\n    add: '添加',\n    added: '已添加',\n    more: '更多',\n    edit: '编辑',\n    clear: '清理',\n    update: '更新',\n    delete: '删除',\n    cancel: '取消',\n    save: '保存',\n    nextStep: '下一步',\n    prevStep: '上一步',\n    disabled: '已禁用',\n    enabled: '已启用',\n    preview: '预览',\n    warning: '警告',\n    disable: '禁用',\n    enable: '启用',\n    use: '使用',\n    none: '无',\n    close: '关闭',\n    reset: '重置',\n    pause: '暂停',\n    resume: '恢复',\n    details: '详情',\n    updateAll: '更新全部',\n    updateTime: '更新时间',\n    keywords: '关键词',\n    success: '成功',\n    copy: '复制',\n    copied: '已复制',\n    auto: '自动',\n    import: '导入',\n    install: '安装',\n    uninstall: '卸载',\n    run: '运行',\n    refresh: '刷新',\n    confirm: '确定',\n    selectAll: '全选',\n    http: '远程下载',\n    file: '本地文件',\n    openFile: '打开文件',\n    develop: '开发',\n    canceled: '已取消',\n    downloading: '下载中...',\n    empty: '数据为空',\n    pressAgainToClose: '再按一次关闭弹窗',\n  },\n  kernel: {\n    rule: '规则',\n    global: '全局',\n    direct: '直连',\n    ruleDesc: '按照规则文件分流',\n    globalDesc: '仅走Global策略组',\n    directDesc: '直接连接所有流量',\n    log: {\n      disabled: '禁用日志',\n      level: '日志级别',\n      output: '日志保存路径',\n      timestamp: '日志时间戳',\n      trace: '跟踪',\n      debug: '调试',\n      info: '信息',\n      warn: '警告',\n      error: '错误',\n      fatal: '致命',\n      panic: '恐慌',\n    },\n    clash_api: {\n      external_controller: 'RESTful Web API监听地址',\n      external_ui: 'Web UI路径',\n      external_ui_download_url: 'Web UI下载地址',\n      external_ui_download_detour: 'Web UI下载地址的出站标签',\n      secret: 'RESTful API密钥',\n      default_mode: '工作模式',\n      access_control_allow_origin: '允许的CORS来源',\n      access_control_allow_private_network: '允许从私有网络访问',\n    },\n    cache_file: {\n      enabled: '启用缓存',\n      path: '缓存文件路径',\n      cache_id: '缓存文件中的标识符',\n      store_fakeip: '持久化FakeIP',\n      store_rdrc: '持久化已拒绝的DNS响应',\n      rdrc_timeout: '拒绝的DNS响应缓存超时',\n    },\n    inbounds: {\n      enable: '启用',\n      tag: '名称',\n      users: 'Http/Socks验证用户',\n      listen: {\n        listen: '监听地址',\n        listen_port: '端口',\n        tcp_fast_open: 'TCP快速打开',\n        tcp_multi_path: '多路径TCP',\n        udp_fragment: 'UDP分段',\n      },\n      tun: {\n        interface_name: 'TUN网卡名称',\n        address: 'IPv4和IPv6前缀',\n        mtu: '最大传输单元',\n        auto_route: '自动设置全局路由',\n        strict_route: '严格路由',\n        route_address: '自定义路由',\n        route_exclude_address: '排除自定义路由',\n        endpoint_independent_nat: '独立于端点的 NAT',\n        stack: 'TUN模式堆栈',\n        system: 'System',\n        gvisor: 'gVisor',\n        mixed: 'Mixed',\n      },\n      mixedPort: '混合代理端口',\n      httpPort: 'HTTP(s)代理端口',\n      socksPort: 'SOCKS5代理端口',\n    },\n    outbounds: {\n      name: '出站',\n      tag: '名称',\n      type: '类型',\n      url: '测延迟链接',\n      interval: '测试间隔(m)',\n      tolerance: '测试容差(ms)',\n      interrupt_exist_connections: '中断现有连接',\n      direct: '直连',\n      block: '阻断',\n      directDesc: '无设置项目',\n      selector: '手动选择',\n      urltest: '自动选择',\n      notFound: '部分出站或代理已丢失，请清理',\n      needToAdd: '至少引用一个出站或订阅',\n      refsSubscription: '引用订阅',\n      refsOutbound: '引用出站',\n      sort: '查看和排序',\n      refs: '引用订阅&引用节点',\n      noSubs: '订阅列表为空',\n      empty: '该订阅下无可用代理',\n      builtIn: '内置',\n      subscriptions: '订阅',\n      include: '包含',\n      exclude: '排除',\n    },\n    route: {\n      tab: {\n        common: '通用',\n        rules: '规则',\n        rule_set: '规则集',\n      },\n      find_process: '查找进程信息',\n      auto_detect_interface: '自动检测出站接口',\n      default_interface: '出站接口名称',\n      final: '默认出站标签',\n      default_domain_resolver: {\n        server: '解析节点域名的DNS服务器',\n        client_subnet: '客户端子网',\n      },\n      rule_set: {\n        type: {\n          name: '类型',\n          inline: '内联',\n          local: '本地',\n          remote: '远程',\n        },\n        tag: '名称',\n        format: {\n          name: '格式',\n          binary: '二进制',\n          source: '源文件',\n        },\n        url: '远程链接',\n        download_detour: '下载方式',\n        update_interval: '自动更新间隔',\n        path: '保存路径',\n        notFound: '规则集已丢失',\n        empty: '规则集列表为空',\n      },\n      rules: {\n        type: '规则类型',\n        action: {\n          name: '规则动作',\n          route: '路由',\n          'route-options': '路由设置选项',\n          reject: '拒绝连接',\n          predefined: '预定义',\n          'hijack-dns': '劫持DNS请求',\n          sniff: '协议嗅探',\n          resolve: '解析DNS',\n          rejectMethod: '拒绝方式',\n          rejectDefault: 'default',\n          rejectDrop: 'drop',\n          rejectReply: 'reply',\n        },\n        outbound: '出站标签',\n        routeOptions: '路由选项',\n        sniffer: {\n          name: '探测器',\n          http: 'http',\n          tls: 'tls',\n          quic: 'quic',\n          stun: 'stun',\n          dns: 'dns',\n          bittorrent: 'bittorrent',\n          dtls: 'dtls',\n          ssh: 'ssh',\n          rdp: 'rdp',\n          ntp: 'ntp',\n        },\n        server: 'DNS服务器',\n        payload: '载荷',\n        strategy: '解析策略',\n        disable_cache: '禁用DNS缓存',\n        client_subnet: '客户端子网',\n        notFound: '出站标签缺失。',\n        invalid: '无效参数',\n        invert: '反向匹配',\n      },\n    },\n    rules: {\n      type: {\n        name: '类型',\n        inbound: '入站(inbound)',\n        network: '网络(network)',\n        protocol: '协议(protocol)',\n        domain: '域名(domain)',\n        domain_suffix: '域名后缀(domain_suffix)',\n        domain_keyword: '域名关键词(domain_keyword)',\n        domain_regex: '域名正则(domain_regex)',\n        source_ip_cidr: '源IP地址段(source_ip_cidr)',\n        ip_cidr: 'IP地址段(ip_cidr)',\n        ip_is_private: '是否为私有IP(ip_is_private)',\n        source_port: '源端口(source_port)',\n        source_port_range: '源端口范围(source_port_range)',\n        port: '端口(port)',\n        port_range: '端口范围(port_range)',\n        process_name: '进程名称(process_name)',\n        process_path: '进程路径(process_path)',\n        process_path_regex: '进程路径正则(process_path_regex)',\n        clash_mode: 'Clash模式(clash_mode)',\n        rule_set: '规则集(rule_set)',\n        ip_accept_any: '匹配任意 IP(ip_accept_any)',\n        inline: '内联(Inline)',\n      },\n    },\n    strategy: {\n      name: '策略',\n      default: '默认',\n      byDnsRules: '由DNS路由规则决定',\n      prefer_ipv4: 'IPV4优先',\n      prefer_ipv6: 'IPV6优先',\n      ipv4_only: '只使用IPV4',\n      ipv6_only: '只使用IPV6',\n    },\n    dns: {\n      tab: {\n        common: '通用',\n        servers: '服务器',\n        rules: '规则',\n      },\n      tag: '名称',\n      type: {\n        name: '类型',\n        local: '本地',\n        hosts: 'Hosts',\n        tcp: 'TCP',\n        udp: 'UDP',\n        tls: 'TLS',\n        https: 'HTTPS',\n        quic: 'QUIC',\n        h3: 'HTTP/3',\n        predefined: '预定义',\n        dhcp: 'DHCP',\n        fakeip: 'Fake-IP',\n      },\n      detour: '出站标签',\n      server: '地址',\n      server_port: '端口',\n      domain_resolver: '解析本DNS服务器域名的DNS',\n      path: '路径',\n      interface: '接口名称',\n      disable_cache: '禁用DNS缓存',\n      client_subnet: '客户端子网',\n      disable_expire: '禁用DNS缓存过期',\n      independent_cache: '独立缓存',\n      final: '回退DNS',\n      strategy: '解析策略',\n      inet4_range: 'Fake-IP范围(IPv4)',\n      inet6_range: 'Fake-IP范围(IPv6)',\n      hosts_path: 'Hosts文件路径',\n      predefined: '预定义',\n      rules: {\n        type: '类型',\n        payload: '载荷',\n        action: '规则动作',\n        server: '目标DNS服务器的标签',\n      },\n    },\n    mode: '工作模式',\n    'allow-lan': '允许局域网访问',\n    'disallow-lan': '禁止局域网访问',\n    notFound: '无核心',\n    insertionPoint: '新规则将插入到这里',\n    addInsertionPoint: '添加插入点',\n  },\n  router: {\n    overview: '概览',\n    subscriptions: '订阅',\n    rulesets: '规则集',\n    plugins: '插件',\n    settings: '设置',\n    about: '关于',\n    profiles: '配置',\n    kernel: '核心',\n    scheduledtasks: '计划任务',\n  },\n  home: {\n    mode: '代理模式',\n    global: '全局',\n    rule: '规则',\n    direct: '直连',\n    quickStart: '快速开始',\n    noProfile: '欢迎使用 {0}，点击按钮开始。',\n    initSuccessful: '初始化配置、订阅成功',\n    overview: {\n      expandAll: '展开全部',\n      collapseAll: '收缩全部',\n      refresh: '刷新',\n      delayTest: '延迟测试',\n      stop: '停止核心',\n      restart: '重启核心',\n      viewlog: '查看日志',\n      start: '启动核心',\n      noLogs: '日志为空',\n      systemProxy: '系统代理',\n      tunMode: 'TUN模式',\n      traffic: '流量',\n      realtimeTraffic: '实时流量',\n      totalTraffic: '总流量',\n      connections: '活动连接',\n      memory: '内存',\n      transmit: '上行速率',\n      receive: '下行速率',\n      settings: '核心设置',\n      settingsTips: '暂时生效，持久化请修改配置文件',\n      updateGEO: '更新 GEO',\n      needPort: '请先添加一个Mixed/Http/Socks入站',\n      needTun: '请先添加一个TUN入站',\n    },\n    controller: {\n      name: '控制器',\n      autoClose: '自动断开连接',\n      unAvailable: '展示不可用节点',\n      cardMode: '卡片模式',\n      sortBy: '按延迟排序',\n      delay: '延迟测试URL',\n      timeout: '延迟测试超时时间(ms)',\n      concurrencyLimit: '延迟测试并发数量',\n      cardColumns: '卡片展示列数',\n      sensitivity: '控制器滚动灵敏度',\n      closeMode: {\n        name: '控制器关闭模式',\n        all: '滚动和关闭按钮',\n        button: '仅按钮',\n      },\n    },\n    connections: {\n      type: '类型',\n      processPath: '进程路径',\n      sourceIP: '源地址',\n      destinationIP: '目标IP',\n      host: '主机',\n      inbound: '入站模式',\n      rule: '匹配规则',\n      chains: '链路',\n      upload: '上行流量',\n      download: '下行流量',\n      uploadSpeed: '上行速度',\n      downSpeed: '下行速度',\n      time: '连接时间',\n      close: '关闭连接',\n      addToDirect: '添加到直连',\n      addToProxy: '添加到代理',\n      addToReject: '添加到拦截',\n      active: '活动',\n      closed: '已关闭',\n      closeAll: '关闭所有连接',\n      sort: '排序和设置显示字段',\n      details: '连接详情',\n    },\n  },\n  subscribe: {\n    manual: '手动管理',\n    name: '名称',\n    url: '远程链接',\n    localPath: '本地路径',\n    website: '官网',\n    path: '保存路径',\n    include: '包括名称',\n    exclude: '排除名称',\n    includeProtocol: '包括协议',\n    excludeProtocol: '排除协议',\n    proxyPrefix: '代理前缀',\n    updating: '更新中',\n    useragent: '用户代理',\n    inSecure: '跳过证书验证',\n    requestMethod: '请求方式',\n    requestTimeout: '请求超时时间(秒)',\n    header: {\n      request: '请求头',\n      response: '响应头',\n    },\n  },\n  subscribes: {\n    download: '下行流量',\n    upload: '上行流量',\n    total: '总流量',\n    expire: '过期时间',\n    subtype: '订阅类型',\n    website: '官网',\n    empty: '订阅列表为空，请先{action}订阅。',\n    enterLink: '输入订阅链接',\n    proxyCount: '代理数量',\n    editProxies: '编辑节点',\n    editSourceFile: '编辑节点(源文件)',\n    copySub: '复制订阅链接',\n    script: '脚本',\n    proxies: {\n      type: '协议',\n      name: '名称',\n      add: '添加代理',\n    },\n  },\n  profile: {\n    name: '名称',\n    generalSettings: '通用设置',\n    advancedSettings: '高级设置',\n    step: {\n      name: '名称设置',\n      general: '通用设置',\n      inbounds: '入站设置',\n      outbounds: '出站设置',\n      route: '路由设置',\n      dns: 'DNS设置',\n      'mixin-script': '混入和脚本',\n    },\n    proxies: '引用节点',\n    use: '引用订阅',\n    noSubs: '没有可用的订阅',\n    group: '策略组详情',\n    rule: '规则详情',\n    auto: '此配置由订阅接管，更新订阅时会被覆盖！\\n如果你想修改此配置，请使用插件系统。',\n    mixinSettings: {\n      name: '混入配置',\n      priority: '优先级',\n      format: '格式',\n      mixin: '混入优先',\n      gui: 'GUI优先',\n    },\n    scriptSettings: {\n      name: '脚本操作',\n    },\n  },\n  profiles: {\n    shouldStop: '当前配置正在使用，无法删除',\n    empty: '配置列表为空，请先{action}配置。',\n    copytoClipboard: '生成配置到剪切板',\n    generateAndView: '生成配置并查看',\n    copy: '复制并粘贴',\n    start: '使用此配置启动/重启',\n    inbounds: '入站',\n    outbounds: '出站',\n    dnsServers: 'DNS服务器',\n    dnsRules: 'DNS规则',\n  },\n  ruleset: {\n    manual: '手动管理',\n    format: {\n      name: '文件格式',\n      source: '源文件',\n      binary: '二进制',\n    },\n    rulesetType: '规则集类型',\n    name: '名称',\n    url: '远程链接',\n    path: '保存路径',\n    interval: '更新间隔',\n    updating: '更新中',\n  },\n  rulesets: {\n    hub: '规则集中心',\n    total: '规则集数量为',\n    noDesc: '无描述信息',\n    updating: '更新中',\n    updateSuccess: '规则集中心更新成功',\n    fetching: '获取中...',\n    empty: '规则集列表为空，请先{action}或从{import}导入。',\n    rulesetCount: '规则数量',\n    editRuleset: '编辑规则',\n    selectRuleType: '选择规则类型',\n  },\n  plugin: {\n    trigger: '触发器',\n    'on::manual': '手动触发',\n    'on::startup': '启动APP时',\n    'on::shutdown': '关闭APP时',\n    'on::generate': '生成配置时',\n    'on::subscribe': '更新订阅时',\n    'on::ready': 'APP就绪后',\n    'on::reload': 'APP重载时',\n    'on::task': '计划任务执行时',\n    'on::install': '点击安装时',\n    'on::uninstall': '点击卸载时',\n    'on::configure': '配置插件时',\n    'on::core::started': '核心启动后',\n    'on::core::stopped': '核心停止后',\n    'on::before::core::start': '核心启动前',\n    'on::before::core::stop': '核心停止前',\n    'on::tray::update': '托盘更新时',\n    name: '名称',\n    version: '版本号',\n    description: '描述',\n    url: '远程地址',\n    install: '是否需要安装',\n    installed: '已安装',\n    path: '保存路径',\n    type: '类型',\n    menus: '菜单',\n    hasUI: '是否具有用户界面',\n    context: '上下文',\n    configuration: '配置',\n    menuKey: '菜单名称',\n    menuValue: '触发方法名',\n    selectComponent: '请选择一个组件',\n    confName: '配置名',\n    confDescription: '配置描述',\n    confKey: '配置标志',\n    confDefault: '默认值',\n    options: '选项',\n    restore: '恢复为默认值',\n  },\n  plugins: {\n    updating: '更新中',\n    empty: '插件列表为空，请先{action}或从{import}导入。',\n    source: '源码',\n    reload: '重载插件',\n    configuration: '配置插件',\n    hub: '插件中心',\n    update: '更新列表',\n    checkForUpdates: '检查更新',\n    updateSuccess: '插件中心更新成功',\n    total: '插件数量为',\n    removeConfiguration: '是否删除插件配置？',\n    testRun: '运行测试',\n    deprecated: '已废弃',\n    newVersion: '新版本',\n  },\n  scheduledtask: {\n    name: '名称',\n    type: '任务类型',\n    script: '脚本代码',\n    subscriptions: '订阅列表',\n    rulesets: '规则集列表',\n    plugins: '插件列表',\n    cron: '表达式',\n    notification: '任务完成通知',\n    cronTips: '秒 分 时 日 月 星期',\n    lastTime: '上次执行时间',\n    'update::subscription': '更新订阅',\n    'update::ruleset': '更新规则集',\n    'update::plugin': '更新插件',\n    'update::all::subscription': '更新所有订阅',\n    'update::all::ruleset': '更新所有规则集',\n    'update::all::plugin': '更新所有插件',\n    'run::plugin': '运行插件',\n    'run::script': '运行脚本',\n  },\n  scheduledtasks: {\n    logs: '日志',\n    name: '插件',\n    duration: '持续时间',\n    startTime: '开始时间',\n    endTime: '结束时间',\n    time: '执行时间',\n    result: '执行结果',\n    empty: '计划任务列表为空，请先{action}计划任务。',\n    run: '立即运行',\n    log: '查看日志',\n    next: '下次运行时间',\n  },\n  settings: {\n    personalization: '个性化',\n    behavior: '行为',\n    systemProxy: '系统代理',\n    advanced: '高级',\n    features: '特性',\n    general: '通用',\n    theme: {\n      name: '主题',\n      light: '浅色',\n      dark: '深色',\n      auto: '跟随系统',\n    },\n    color: {\n      name: '颜色',\n      default: '默认',\n      green: '绿色',\n      purple: '紫色',\n      custom: '自定义',\n      primary: '主色',\n      secondary: '辅助色',\n    },\n    fontFamily: '字体',\n    resetFont: '重置字体',\n    appFolder: {\n      name: '应用程序文件夹',\n      open: '打开应用程序文件夹',\n    },\n    lang: {\n      name: '语言',\n      load: '加载语言文件',\n      zh: '简体中文',\n      en: 'English',\n    },\n    pages: {\n      name: '页面可见性',\n    },\n    windowState: {\n      normal: '以普通窗口启动',\n      maximised: '最大化',\n      minimised: '最小化窗口启动',\n      fullscreen: '全屏',\n    },\n    webviewGpuPolicy: {\n      name: 'Webview GPU 策略',\n      always: '启用硬件加速',\n      onDemand: '根据Web内容自行决定',\n      never: '禁用硬件加速',\n    },\n    needRestart: '重启生效',\n    needAdmin: '需要管理员权限',\n    exitOnClose: '关闭窗口时退出程序',\n    closeKernelOnExit: '程序退出时关闭核心',\n    autoSetSystemProxy: '自动配置系统代理',\n    proxyBypassList: '不使用代理的地址',\n    proxyBypassListTips: '分号分隔',\n    autoStartKernel: '程序启动时开启核心',\n    realMemoryUsage: '显示真实的核心内存占用',\n    autoRestartKernel: {\n      name: '相关配置变化时自动重启核心',\n      tips: '会中断所有连接，且可能重启失败',\n    },\n    admin: '以管理员身份运行',\n    addPluginToMenu: '将插件添加到托盘菜单',\n    addGroupToMenu: '将代理组添加到托盘菜单',\n    multipleInstance: '允许多APP实例运行',\n    rollingRelease: '启用滚动发行',\n    debugOutline: '组件轮廓',\n    debugNoAnimation: '禁用动画',\n    debugNoRounded: '禁用圆角',\n    debugBorder: '显示窗口边框',\n    startup: {\n      name: '自启动',\n      delay: '延迟(秒)',\n      startupDelay: '自启动延迟',\n    },\n    kernel: {\n      name: 'sing-box',\n      version: '切换版本',\n      stable: '稳定版',\n      alpha: '内测版',\n      grant: '授予特权',\n      openTip: '打开文件所在位置',\n      linkTip: '在GitHub上查看发布版本',\n      local: '本地',\n      remote: '远程',\n      update: '更新',\n      restart: '重启核心',\n      risk: '该版本非GitHub自动构建，有安全风险。',\n      stillDownload: '仍要下载',\n      rollbackTip: '回滚到上一版本',\n      rollback: '确定回滚到上一版本吗？',\n      clearCache: '清除缓存',\n      config: {\n        name: '运行时配置',\n        env: '环境变量',\n        args: '运行参数',\n      },\n    },\n    plugin: {\n      resetSetting: '重置设置',\n      resetSettings: '重置所有设置',\n    },\n    userAgent: {\n      name: '用户代理(User-Agent)',\n      reset: ' 重置用户代理',\n      tips: '用于此应用程序的网络请求',\n    },\n    githubapi: {\n      name: 'GitHub REST API 访问令牌',\n      tips: '可获得更高的速率限制',\n    },\n  },\n  about: {\n    new: '新版本',\n    restart: '重启软件',\n    noDownloadLink: '没有发现下载链接',\n    updateSuccessfulRestart: '更新完成，请重启软件',\n    updateSuccessfulReplace: '下载完成，请手动替换软件',\n    updateSuccessful: '更新完成',\n    newVersion: '发现新版本',\n    latestVersion: '已经是最新版本了',\n  },\n  titlebar: {\n    resetSize: '重置窗口',\n    reload: '重载界面',\n    restart: '重启程序',\n    exitApp: '退出程序',\n    exitPending: '正在等待程序退出...',\n    exitTimeout: '程序退出超时，是否强制退出？',\n    exitError: '退出时发生错误，是否强制退出？\\n\\n原因：{reason}',\n    reloadPending: '正在等待程序重载...',\n    reloadTimeout: '程序重载超时，是否强制重载？',\n    reloadError: '重载时发生错误，是否强制重载？\\n\\n原因：{reason}',\n  },\n  outbound: {\n    select: '🚀 节点选择',\n    urltest: '🎈 自动选择',\n    direct: '🎯 全球直连',\n    block: '🛑 全球拦截',\n    fallback: '🐟 漏网之鱼',\n  },\n  tray: {\n    showMainWindow: '显示主窗口',\n    restart: '重启',\n    restartTip: '重启程序',\n    exit: '退出',\n    exitTip: '退出程序',\n    proxyGroup: '代理组',\n    setSystemProxy: '设置系统代理',\n    clearSystemProxy: '清除系统代理',\n    tun: 'Tun模式',\n    enableTunMode: '启用TUN模式',\n    disableTunMode: '禁用TUN模式',\n    kernel: '核心管理',\n    proxy: '系统代理',\n    startKernel: '开启核心',\n    stopKernel: '关闭核心',\n    restartKernel: '重启核心',\n    plugins: '插件',\n  },\n  commands: {\n    noMatching: '没有匹配到命令',\n  },\n}\n"
  },
  {
    "path": "frontend/src/main.ts",
    "content": "import { createPinia } from 'pinia'\nimport { createApp } from 'vue'\n\nimport './assets/main.less'\nimport './assets/polyfills'\nimport './assets/globalMethods'\n\nimport App from './App.vue'\nimport components from './components'\nimport directives from './directives'\nimport i18n from './lang'\nimport router from './router'\n\nconst app = createApp(App)\n\nwindow.appInstance = app\n\napp.use(createPinia())\napp.use(router)\napp.use(i18n)\napp.use(components)\napp.use(directives)\n\napp.mount('#app')\n"
  },
  {
    "path": "frontend/src/router/index.ts",
    "content": "import { createRouter, createWebHashHistory } from 'vue-router'\n\nimport routes from './routes'\n\nconst router = createRouter({\n  history: createWebHashHistory(import.meta.env.BASE_URL),\n  routes,\n})\n\nexport default router\n"
  },
  {
    "path": "frontend/src/router/router.d.ts",
    "content": "import 'vue-router'\n\nimport { type IconType } from '@/components/Icon/index.vue'\n\ndeclare module 'vue-router' {\n  interface RouteMeta {\n    name: string\n    icon?: IconType\n    hidden?: boolean\n  }\n}\n"
  },
  {
    "path": "frontend/src/router/routes.ts",
    "content": "import { type RouteRecordRaw } from 'vue-router'\n\nconst routes: RouteRecordRaw[] = [\n  {\n    path: '/',\n    name: 'Overview',\n    component: () => import('@/views/HomeView/index.vue'),\n    meta: {\n      name: 'router.overview',\n      icon: 'overview',\n    },\n  },\n  {\n    path: '/profiles',\n    name: 'Profiles',\n    component: () => import('@/views/ProfilesView/index.vue'),\n    meta: {\n      name: 'router.profiles',\n      icon: 'profiles',\n    },\n  },\n  {\n    path: '/subscriptions',\n    name: 'Subscriptions',\n    component: () => import('@/views/SubscribesView/index.vue'),\n    meta: {\n      name: 'router.subscriptions',\n      icon: 'subscriptions',\n    },\n  },\n  {\n    path: '/rulesets',\n    name: 'Rulesets',\n    component: () => import('@/views/RulesetsView/index.vue'),\n    meta: {\n      name: 'router.rulesets',\n      icon: 'rulesets',\n    },\n  },\n  {\n    path: '/plugins',\n    name: 'Plugins',\n    component: () => import('@/views/PluginsView/index.vue'),\n    meta: {\n      name: 'router.plugins',\n      icon: 'plugins',\n    },\n  },\n  {\n    path: '/scheduledtasks',\n    name: 'ScheduledTasks',\n    component: () => import('@/views/ScheduledTasksView/index.vue'),\n    meta: {\n      name: 'router.scheduledtasks',\n      icon: 'scheduledTasks',\n    },\n  },\n  {\n    path: '/settings',\n    name: 'Settings',\n    component: () => import('@/views/SettingsView/index.vue'),\n    meta: {\n      name: 'router.settings',\n      icon: 'settings2',\n      hidden: false,\n    },\n  },\n]\n\nexport default routes\n"
  },
  {
    "path": "frontend/src/stores/app.ts",
    "content": "import { defineStore } from 'pinia'\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport {\n  Download,\n  HttpGet,\n  MoveFile,\n  UnzipZIPFile,\n  MakeDir,\n  RemoveFile,\n  HttpCancel,\n  OpenDir,\n  ReadDir,\n} from '@/bridge'\nimport { LanguageOptions, LocalesFilePath, RollingReleaseDirectory } from '@/constant/app'\nimport { loadLocale } from '@/lang'\nimport {\n  APP_TITLE,\n  APP_VERSION,\n  APP_VERSION_API,\n  getGitHubApiAuthorization,\n  ignoredError,\n  message,\n  alert,\n  sampleID,\n  sleep,\n} from '@/utils'\n\nimport { useEnvStore } from './env'\n\nimport type { CustomAction, CustomActionFn, Menu } from '@/types/app'\n\nexport const useAppStore = defineStore('app', () => {\n  const isAppExiting = ref(false)\n  const isAppReloading = ref(false)\n\n  /* Global Menu */\n  const menuShow = ref(false)\n  const menuList = ref<Menu[]>([])\n  const menuPosition = ref({\n    x: 0,\n    y: 0,\n  })\n\n  /* Global Tips */\n  const tipsShow = ref(false)\n  const tipsMessage = ref('')\n  const tipsPosition = ref({\n    x: 0,\n    y: 0,\n  })\n\n  /* Modal Stack */\n  const modalStack: (() => void)[] = []\n  const modalZIndexCounter = 999\n\n  /* i18n */\n  const localesLoading = ref(false)\n  const locales = ref<{ label: string; value: string }[]>([])\n  const loadLocales = async (delay = true, reload = true) => {\n    localesLoading.value = true\n    const dirs = await ReadDir(LocalesFilePath).catch(() => [])\n    const localLanguage = dirs.flatMap((file) => {\n      if (file.isDir) return []\n      const [name, ext] = file.name.split('.')\n      return name && ext === 'json' ? { label: name, value: name } : []\n    })\n    locales.value = [...LanguageOptions, ...localLanguage]\n    reload && (await loadLocale())\n    delay && (await sleep(200))\n    localesLoading.value = false\n  }\n\n  /* Actions */\n  const customActions = ref({\n    core_state: [] as (CustomAction | CustomActionFn)[],\n    title_bar: [] as (CustomAction | CustomActionFn)[],\n    profiles_header: [] as (CustomAction | CustomActionFn)[],\n    subscriptions_header: [] as (CustomAction | CustomActionFn)[],\n  })\n  const addCustomActions = (\n    target: keyof typeof customActions.value,\n    actions: CustomAction | CustomAction[] | CustomActionFn | CustomActionFn[],\n  ) => {\n    if (!customActions.value[target]) throw new Error('Target does not exist: ' + target)\n    const _actions = Array.isArray(actions) ? actions : [actions]\n    _actions.forEach((action) => !action.id && (action.id = sampleID()))\n    customActions.value[target].push(..._actions)\n    const remove = () => {\n      customActions.value[target] = customActions.value[target].filter(\n        (a) => !_actions.some((added) => added.id === a.id),\n      )\n    }\n    return remove\n  }\n  const removeCustomActions = (target: keyof typeof customActions.value, id: string | string[]) => {\n    if (!customActions.value[target]) throw new Error('Target does not exist: ' + target)\n    const ids = Array.isArray(id) ? id : [id]\n    customActions.value[target] = customActions.value[target].filter((a) => !ids.includes(a.id!))\n  }\n\n  const { t } = useI18n()\n  const envStore = useEnvStore()\n\n  /* About Page */\n  const showAbout = ref(false)\n  const checkForUpdatesLoading = ref(false)\n  const restartable = ref(false)\n  const downloading = ref(false)\n  const downloadUrl = ref('')\n  const remoteVersion = ref(APP_VERSION)\n  const updatable = computed(() => downloadUrl.value && APP_VERSION !== remoteVersion.value)\n\n  const downloadApp = async () => {\n    downloading.value = true\n    try {\n      const downloadCacheFile = 'data/.cache/gui.zip'\n      const downloadCancelId = downloadCacheFile\n\n      const { update, destroy } = message.info('common.downloading', 10 * 60 * 1_000, () => {\n        HttpCancel(downloadCancelId)\n        setTimeout(() => RemoveFile(downloadCacheFile), 1000)\n      })\n\n      await MakeDir('data/.cache')\n      await Download(\n        downloadUrl.value,\n        downloadCacheFile,\n        undefined,\n        (progress, total) => {\n          update(t('common.downloading') + ((progress / total) * 100).toFixed(2) + '%')\n        },\n        {\n          CancelId: downloadCancelId,\n        },\n      ).finally(destroy)\n\n      const { appName, os } = envStore.env\n      if (os !== 'darwin') {\n        await MoveFile(appName, appName + '.bak')\n        await UnzipZIPFile(downloadCacheFile, '.')\n        const suffix = { windows: '.exe', linux: '' }[os]\n        await MoveFile(APP_TITLE + suffix, appName)\n        message.success('about.updateSuccessfulRestart')\n        restartable.value = true\n      } else {\n        await UnzipZIPFile(downloadCacheFile, 'data')\n        alert('common.success', 'about.updateSuccessfulReplace')\n        await OpenDir('data')\n      }\n\n      await RemoveFile(downloadCacheFile)\n      await ignoredError(RemoveFile, RollingReleaseDirectory)\n    } catch (error: any) {\n      console.log(error)\n      message.error(error.message || error, 5_000)\n    }\n    downloading.value = false\n  }\n\n  const checkForUpdates = async (showTips = false) => {\n    if (checkForUpdatesLoading.value || downloading.value) return\n    checkForUpdatesLoading.value = true\n    remoteVersion.value = APP_VERSION\n    try {\n      const { body } = await HttpGet<Record<string, any>>(APP_VERSION_API, {\n        Authorization: getGitHubApiAuthorization(),\n      })\n      if (body.message) throw body.message\n\n      const { tag_name, assets } = body\n\n      const { os, arch } = envStore.env\n      const assetName = `${APP_TITLE}-${os}-${arch}.zip`\n\n      const asset = assets.find((v: any) => v.name === assetName)\n      if (!asset) throw 'Asset Not Found:' + assetName\n\n      remoteVersion.value = tag_name\n      downloadUrl.value = asset.browser_download_url\n\n      if (showTips) {\n        message.info(updatable.value ? 'about.newVersion' : 'about.latestVersion')\n      }\n    } catch (error: any) {\n      console.error(error)\n      message.error(error.message || error)\n    }\n    checkForUpdatesLoading.value = false\n  }\n\n  return {\n    isAppExiting,\n    isAppReloading,\n    menuShow,\n    menuPosition,\n    menuList,\n    tipsShow,\n    tipsMessage,\n    tipsPosition,\n    modalStack,\n    modalZIndexCounter,\n    showAbout,\n    checkForUpdatesLoading,\n    restartable,\n    downloading,\n    remoteVersion,\n    updatable,\n    checkForUpdates,\n    downloadApp,\n    customActions,\n    addCustomActions,\n    removeCustomActions,\n    localesLoading,\n    locales,\n    loadLocales,\n  }\n})\n"
  },
  {
    "path": "frontend/src/stores/appSettings.ts",
    "content": "import { defineStore } from 'pinia'\nimport { ref, watch } from 'vue'\nimport { parse, stringify } from 'yaml'\n\nimport {\n  ReadFile,\n  WriteFile,\n  WindowSetSystemDefaultTheme,\n  WindowIsMaximised,\n  WindowIsMinimised,\n} from '@/bridge'\nimport {\n  Colors,\n  DefaultCardColumns,\n  DefaultConcurrencyLimit,\n  DefaultControllerSensitivity,\n  DefaultFontFamily,\n  DefaultTestTimeout,\n  DefaultTestURL,\n  UserFilePath,\n} from '@/constant/app'\nimport { DefaultConnections, DefaultCoreConfig } from '@/constant/kernel'\nimport {\n  Theme,\n  WindowStartState,\n  Lang,\n  View,\n  Color,\n  WebviewGpuPolicy,\n  ControllerCloseMode,\n  Branch,\n} from '@/enums/app'\nimport i18n, { loadLocale } from '@/lang'\nimport { useAppStore, useEnvStore } from '@/stores'\nimport {\n  debounce,\n  updateTrayAndMenus,\n  ignoredError,\n  GetSystemProxyBypass,\n  deepClone,\n} from '@/utils'\n\nimport type { AppSettings } from '@/types/app'\n\nexport const useAppSettingsStore = defineStore('app-settings', () => {\n  const appStore = useAppStore()\n  const envStore = useEnvStore()\n\n  let latestUserSettings: string\n\n  const app = ref<AppSettings>({\n    lang: Lang.EN,\n    theme: Theme.Auto,\n    color: Color.Default,\n    primaryColor: '#000',\n    secondaryColor: '#545454',\n    fontFamily: DefaultFontFamily,\n    profilesView: View.Grid,\n    subscribesView: View.Grid,\n    rulesetsView: View.Grid,\n    pluginsView: View.Grid,\n    scheduledtasksView: View.Grid,\n    windowStartState: WindowStartState.Normal,\n    webviewGpuPolicy: WebviewGpuPolicy.OnDemand,\n    width: 0,\n    height: 0,\n    exitOnClose: true,\n    closeKernelOnExit: true,\n    autoSetSystemProxy: true,\n    proxyBypassList: '',\n    autoStartKernel: false,\n    autoRestartKernel: false,\n    userAgent: '',\n    startupDelay: 30,\n    connections: DefaultConnections(),\n    kernel: {\n      realMemoryUsage: false,\n      branch: Branch.Main,\n      profile: '',\n      autoClose: true,\n      unAvailable: true,\n      cardMode: true,\n      cardColumns: DefaultCardColumns,\n      sortByDelay: false,\n      testUrl: DefaultTestURL,\n      testTimeout: DefaultTestTimeout,\n      concurrencyLimit: DefaultConcurrencyLimit,\n      controllerCloseMode: ControllerCloseMode.All,\n      controllerSensitivity: DefaultControllerSensitivity,\n      main: undefined as any,\n      alpha: undefined as any,\n    },\n    pluginSettings: {},\n    githubApiToken: '',\n    multipleInstance: false,\n    addPluginToMenu: false,\n    addGroupToMenu: false,\n    rollingRelease: true,\n    debugOutline: false,\n    debugNoAnimation: false,\n    debugNoRounded: false,\n    debugBorder: false,\n    pages: ['Overview', 'Profiles', 'Subscriptions', 'Plugins'],\n  })\n\n  const saveAppSettings = debounce((config: string) => {\n    WriteFile(UserFilePath, config)\n  }, 500)\n\n  const setupAppSettings = async () => {\n    const data = await ignoredError(ReadFile, UserFilePath)\n    let settings: AppSettings\n    if (data) {\n      settings = parse(data)\n    } else {\n      settings = deepClone(app.value)\n    }\n\n    await appStore.loadLocales(false, false)\n\n    if (!settings.kernel.main) {\n      settings.kernel.main = DefaultCoreConfig()\n      settings.kernel.alpha = DefaultCoreConfig()\n    }\n    if (!settings.proxyBypassList) {\n      settings.proxyBypassList = await GetSystemProxyBypass()\n    }\n\n    app.value = settings\n    latestUserSettings = stringify(app.value)\n  }\n\n  const applyAppSettings = {\n    theme(theme: Theme) {\n      const isAuto = theme === Theme.Auto\n      if (isAuto) {\n        themeMode.value = mediaQueryList.matches ? Theme.Dark : Theme.Light\n      } else {\n        themeMode.value = theme\n      }\n    },\n    lang(lang: string) {\n      i18n.global.locale.value = lang\n      if (!i18n.global.availableLocales.includes(lang)) {\n        loadLocale(lang)\n      }\n    },\n    color(color: Color, primary: string, secondary: string) {\n      if (color !== Color.Custom) {\n        ;({ primary, secondary } = Colors[color] ?? { primary, secondary })\n      }\n      document.documentElement.style.setProperty('--primary-color', primary)\n      document.documentElement.style.setProperty('--secondary-color', secondary)\n    },\n    feature(outline: boolean, noAnimation: boolean, noRounded: boolean, border: boolean) {\n      document.body.setAttribute('feature-outline', String(outline))\n      document.body.setAttribute('feature-no-animation', String(noAnimation))\n      document.body.setAttribute('feature-no-rounded', String(noRounded))\n      document.body.setAttribute('feature-border', String(border))\n    },\n    fontFamily(fontFamily: string) {\n      document.body.style.fontFamily = fontFamily\n    },\n    windowSize(width: number, height: number) {\n      app.value.width = width\n      app.value.height = height\n    },\n    systemProxyBypass() {\n      if (envStore.systemProxy) {\n        envStore.setSystemProxy()\n      }\n    },\n  }\n\n  /* Apply AppSettings */\n  const onAppSettingsChange = (settings: AppSettings) => {\n    applyAppSettings.theme(settings.theme)\n    applyAppSettings.color(settings.color, settings.primaryColor, settings.secondaryColor)\n    applyAppSettings.lang(settings.lang)\n    applyAppSettings.fontFamily(settings.fontFamily)\n    applyAppSettings.feature(\n      settings.debugOutline,\n      settings.debugNoAnimation,\n      settings.debugNoRounded,\n      settings.debugBorder,\n    )\n    const lastModifiedSettings = stringify(settings)\n    if (latestUserSettings !== lastModifiedSettings) {\n      saveAppSettings(lastModifiedSettings).then(() => {\n        latestUserSettings = lastModifiedSettings\n      })\n    } else {\n      saveAppSettings.cancel()\n    }\n  }\n  watch(app, onAppSettingsChange, { deep: true })\n\n  /* Apply AppTheme */\n  const themeMode = ref<Theme.Light | Theme.Dark>(Theme.Light)\n  const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)')\n  mediaQueryList.addEventListener('change', ({ matches }) => {\n    if (app.value.theme === Theme.Auto) {\n      themeMode.value = matches ? Theme.Dark : Theme.Light\n    }\n  })\n  const setAppTheme = (theme: Theme.Dark | Theme.Light) => {\n    if (document.startViewTransition) {\n      document.startViewTransition(() => {\n        document.body.setAttribute('theme-mode', theme)\n      })\n    } else {\n      document.body.setAttribute('theme-mode', theme)\n    }\n    WindowSetSystemDefaultTheme()\n  }\n  watch(themeMode, setAppTheme, { immediate: true })\n\n  /* Apply WindowSize */\n  const onWindowSizeChange = debounce(async () => {\n    const [isMinimised, isMaximised] = await Promise.all([WindowIsMinimised(), WindowIsMaximised()])\n    if (!isMinimised && !isMaximised) {\n      const w = document.documentElement.clientWidth\n      const h = document.documentElement.clientHeight\n      applyAppSettings.windowSize(w, h)\n    }\n  }, 1000)\n  window.addEventListener('resize', onWindowSizeChange)\n\n  /* Apply TrayAndMenus */\n  watch(\n    [\n      themeMode,\n      appStore.locales,\n      () => app.value.color,\n      () => app.value.lang,\n      () => app.value.addPluginToMenu,\n    ],\n    updateTrayAndMenus,\n  )\n\n  /* Apply SystemProxyBypass */\n  const setSystemProxyBypass = debounce(() => {\n    applyAppSettings.systemProxyBypass()\n  }, 3000)\n  watch(() => app.value.proxyBypassList, setSystemProxyBypass)\n\n  return { setupAppSettings, app, themeMode }\n})\n"
  },
  {
    "path": "frontend/src/stores/env.ts",
    "content": "import { defineStore } from 'pinia'\nimport { ref, watch } from 'vue'\n\nimport { GetEnv } from '@/bridge'\nimport { useAppSettingsStore, useKernelApiStore } from '@/stores'\nimport { updateTrayAndMenus, SetSystemProxy, GetSystemProxy } from '@/utils'\n\nexport const useEnvStore = defineStore('env', () => {\n  const appSettings = useAppSettingsStore()\n  const kernelApiStore = useKernelApiStore()\n\n  const env = ref({\n    appName: '',\n    appVersion: '',\n    basePath: '',\n    appPath: '',\n    os: '',\n    arch: '',\n    isPrivileged: false,\n  })\n\n  const systemProxy = ref(false)\n\n  const setupEnv = async () => {\n    const _env = await GetEnv()\n    const appPath = `${_env.basePath}/${_env.appName}`\n    env.value = {\n      ..._env,\n      appPath: _env.os === 'windows' ? appPath.replaceAll('/', '\\\\') : appPath,\n    }\n  }\n\n  const updateSystemProxyStatus = async () => {\n    const kernelApiStore = useKernelApiStore()\n    const proxyServer = await GetSystemProxy()\n\n    if (!proxyServer) {\n      systemProxy.value = false\n    } else {\n      const { port, 'mixed-port': mixedPort, 'socks-port': socksPort } = kernelApiStore.config\n      const proxyServerList = [\n        `http://127.0.0.1:${port}`,\n        `http://127.0.0.1:${mixedPort}`,\n\n        `socks5://127.0.0.1:${mixedPort}`,\n        `socks5://127.0.0.1:${socksPort}`,\n\n        `socks=127.0.0.1:${mixedPort}`,\n        `socks=127.0.0.1:${socksPort}`,\n      ]\n      systemProxy.value = proxyServerList.includes(proxyServer)\n    }\n\n    return systemProxy.value\n  }\n\n  const setSystemProxy = async () => {\n    const proxyBypassList = appSettings.app.proxyBypassList\n    let proxyPort = kernelApiStore.getProxyPort()\n\n    if (!proxyPort) {\n      await kernelApiStore.updateConfig('inbound', undefined)\n    }\n\n    proxyPort = kernelApiStore.getProxyPort()\n\n    if (!proxyPort) throw 'home.overview.needPort'\n\n    await SetSystemProxy(true, '127.0.0.1:' + proxyPort.port, proxyPort.proxyType, proxyBypassList)\n\n    systemProxy.value = true\n  }\n\n  const clearSystemProxy = async () => {\n    const proxyBypassList = appSettings.app.proxyBypassList\n    await SetSystemProxy(false, '', undefined, proxyBypassList)\n    systemProxy.value = false\n  }\n\n  const switchSystemProxy = async (enable: boolean) => {\n    if (enable) await setSystemProxy()\n    else await clearSystemProxy()\n  }\n\n  watch(systemProxy, updateTrayAndMenus)\n\n  return {\n    env,\n    setupEnv,\n    systemProxy,\n    setSystemProxy,\n    clearSystemProxy,\n    switchSystemProxy,\n    updateSystemProxyStatus,\n  }\n})\n"
  },
  {
    "path": "frontend/src/stores/index.ts",
    "content": "export * from './appSettings'\nexport * from './profiles'\nexport * from './subscribes'\nexport * from './rulesets'\nexport * from './plugins'\nexport * from './scheduledtasks'\nexport * from './logs'\nexport * from './kernelApi'\nexport * from './app'\nexport * from './env'\n"
  },
  {
    "path": "frontend/src/stores/kernelApi.ts",
    "content": "import { defineStore } from 'pinia'\nimport { computed, ref, watch } from 'vue'\n\nimport {\n  getProxies,\n  getConfigs,\n  setConfigs,\n  onLogs,\n  onMemory,\n  onConnections,\n  onTraffic,\n  initWebsocket,\n  destroyWebsocket,\n} from '@/api/kernel'\nimport { ProcessInfo, KillProcess, ExecBackground, ReadFile, RemoveFile } from '@/bridge'\nimport {\n  CoreConfigFilePath,\n  CorePidFilePath,\n  CoreStopOutputKeyword,\n  CoreWorkingDirectory,\n} from '@/constant/kernel'\nimport { DefaultInboundMixed } from '@/constant/profile'\nimport { Branch } from '@/enums/app'\nimport { Inbound, RulesetType, TunStack } from '@/enums/kernel'\nimport {\n  useAppSettingsStore,\n  useProfilesStore,\n  useLogsStore,\n  useEnvStore,\n  usePluginsStore,\n  useSubscribesStore,\n  useRulesetsStore,\n} from '@/stores'\nimport {\n  generateConfigFile,\n  updateTrayAndMenus,\n  getKernelFileName,\n  restoreProfile,\n  deepClone,\n  message,\n  getKernelRuntimeArgs,\n  getKernelRuntimeEnv,\n  eventBus,\n} from '@/utils'\n\nimport type { CoreApiConfig, CoreApiProxy } from '@/types/kernel'\n\nexport type ProxyType = 'mixed' | 'http' | 'socks'\n\nexport const useKernelApiStore = defineStore('kernelApi', () => {\n  const envStore = useEnvStore()\n  const logsStore = useLogsStore()\n  const pluginsStore = usePluginsStore()\n  const profilesStore = useProfilesStore()\n  const subscribesStore = useSubscribesStore()\n  const rulesetsStore = useRulesetsStore()\n  const appSettingsStore = useAppSettingsStore()\n\n  /** RESTful API */\n  const config = ref<CoreApiConfig>({\n    port: 0,\n    'mixed-port': 0,\n    'socks-port': 0,\n    'interface-name': '',\n    'allow-lan': false,\n    mode: '',\n    tun: {\n      enable: false,\n      stack: '',\n      device: '',\n    },\n  })\n\n  let runtimeProfile: IProfile | undefined\n\n  const proxies = ref<Record<string, CoreApiProxy>>({})\n\n  const refreshConfig = async () => {\n    const _config = await getConfigs()\n\n    config.value = {\n      ..._config,\n      tun: config.value.tun,\n    }\n\n    if (!runtimeProfile) {\n      const txt = await ReadFile(CoreConfigFilePath)\n      runtimeProfile = restoreProfile(JSON.parse(txt))\n      const profile = profilesStore.currentProfile\n      if (profile) {\n        const _profile = deepClone(profile)\n        _profile.inbounds.forEach((inbound) => {\n          const runtimeInbound = runtimeProfile?.inbounds.find((v) => v.tag === inbound.tag)\n          if (runtimeInbound) {\n            runtimeInbound.id = inbound.id\n          } else {\n            inbound.enable = false\n            runtimeProfile?.inbounds.push(inbound)\n          }\n        })\n        runtimeProfile.id = _profile.id\n        runtimeProfile.outbounds = _profile.outbounds\n        runtimeProfile.experimental = _profile.experimental\n        runtimeProfile.dns = _profile.dns\n        runtimeProfile.route = _profile.route\n        runtimeProfile.mixin = _profile.mixin\n        runtimeProfile.script = _profile.script\n      }\n    }\n\n    const mixed = runtimeProfile.inbounds.find((v) => v.enable && v.mixed)\n    const http = runtimeProfile.inbounds.find((v) => v.enable && v.http)\n    const socks = runtimeProfile.inbounds.find((v) => v.enable && v.socks)\n    const tun = runtimeProfile.inbounds.find((v) => v.tun)\n    config.value['mixed-port'] = mixed?.mixed?.listen.listen_port || 0\n    config.value['port'] = http?.http?.listen.listen_port || 0\n    config.value['socks-port'] = socks?.socks?.listen.listen_port || 0\n    config.value['allow-lan'] = [\n      mixed?.mixed?.listen.listen,\n      http?.http?.listen.listen,\n      socks?.socks?.listen.listen,\n    ].some((address) => address === '0.0.0.0' || address === '::')\n\n    config.value.tun.enable = !!tun?.enable\n    config.value.tun.device = tun?.tun?.interface_name || ''\n    config.value.tun.stack = tun?.tun?.stack || ''\n    config.value['interface-name'] = runtimeProfile.route.default_interface\n  }\n\n  const updateConfig = async (field: string, value: any) => {\n    if (field === 'mode') {\n      await setConfigs({ mode: value })\n      await refreshConfig()\n      return\n    }\n\n    const patchInbound = () => {\n      if (!runtimeProfile) return\n      const inbound = runtimeProfile.inbounds.find(\n        (v) =>\n          (v.type === Inbound.Mixed && v.mixed?.listen.listen_port) ||\n          (v.type === Inbound.Http && v.http?.listen.listen_port) ||\n          (v.type === Inbound.Socks && v.socks?.listen.listen_port),\n      )\n      if (!inbound) {\n        throw 'home.overview.needPort'\n      }\n      inbound.enable = true\n    }\n\n    const patchInboundPort = (type: 'mixed' | 'socks' | 'http', port: number) => {\n      if (!runtimeProfile) return\n      let inbound = runtimeProfile.inbounds.find((v) => v.type === type)\n      if (inbound) {\n        inbound[type]!.listen.listen_port = port\n      } else {\n        const _type = DefaultInboundMixed()!\n        _type.listen.listen_port = port\n        inbound = {\n          id: type + '-in',\n          tag: type + '-in',\n          type: type,\n          enable: true,\n          [type]: _type,\n        }\n        runtimeProfile.inbounds.push(inbound)\n      }\n      inbound.enable = port !== 0\n    }\n\n    const patchInboundAddress = (allowLan: boolean) => {\n      if (!runtimeProfile) return\n      runtimeProfile.inbounds.forEach((inbound) => {\n        if (inbound.type === Inbound.Tun) return\n        inbound[inbound.type]!.listen.listen = allowLan ? '0.0.0.0' : '127.0.0.1'\n      })\n    }\n\n    const patchInboundTun = (options: {\n      enable: boolean\n      stack: string\n      device: string\n      interface_name: string\n    }) => {\n      if (!runtimeProfile) return\n      const inbound = runtimeProfile.inbounds.find((v) => v.type === Inbound.Tun)\n      if (!inbound) throw 'home.overview.needTun'\n      options = { ...config.value.tun, ...options }\n      inbound.enable = options.enable\n      inbound.tun!.stack = options.stack || TunStack.Mixed\n      inbound.tun!.interface_name = options.device || ''\n      if (options.interface_name) {\n        runtimeProfile.route.default_interface = options.interface_name\n      }\n      runtimeProfile.route.auto_detect_interface = !options.interface_name\n    }\n\n    const fieldHandlerMap: Recordable<() => void> = {\n      inbound: () => patchInbound(),\n      http: () => patchInboundPort(Inbound.Http, value),\n      socks: () => patchInboundPort(Inbound.Socks, value),\n      mixed: () => patchInboundPort(Inbound.Mixed, value),\n      'allow-lan': () => patchInboundAddress(value),\n      tun: () => patchInboundTun(value),\n      'tun-stack': () => patchInboundTun(value),\n      'tun-device': () => patchInboundTun(value),\n      'interface-name': () => patchInboundTun(value),\n    }\n\n    fieldHandlerMap[field]?.()\n\n    await restartCore(undefined, true)\n    await envStore.updateSystemProxyStatus()\n  }\n\n  const refreshProviderProxies = async () => {\n    const { proxies: b } = await getProxies()\n    proxies.value = b\n  }\n\n  /* Bridge API */\n  const corePid = ref(-1)\n  const running = ref(false)\n  const starting = ref(false)\n  const stopping = ref(false)\n  const restarting = ref(false)\n  const needRestart = ref(false)\n  const coreStateLoading = ref(true)\n  let isCoreStartedByThisInstance = false\n  let { promise: coreStoppedPromise, resolve: coreStoppedResolver } = Promise.withResolvers()\n\n  const initCoreState = async () => {\n    corePid.value = Number(await ReadFile(CorePidFilePath).catch(() => -1))\n    const processName = corePid.value === -1 ? '' : await ProcessInfo(corePid.value).catch(() => '')\n    running.value = processName.startsWith('sing-box')\n\n    coreStateLoading.value = false\n\n    if (running.value) {\n      initWebsocket()\n      await Promise.all([refreshConfig(), refreshProviderProxies()])\n      await envStore.updateSystemProxyStatus()\n    } else if (appSettingsStore.app.autoStartKernel) {\n      await startCore()\n    }\n  }\n\n  const runCoreProcess = (isAlpha: boolean) => {\n    return new Promise<number | void>((resolve, reject) => {\n      let output: string\n      const pid = ExecBackground(\n        CoreWorkingDirectory + '/' + getKernelFileName(isAlpha),\n        getKernelRuntimeArgs(isAlpha),\n        (out) => {\n          output = out\n          logsStore.recordKernelLog(out)\n          if (out.includes(CoreStopOutputKeyword)) {\n            resolve(pid)\n          }\n        },\n        () => {\n          onCoreStopped()\n          reject(output)\n        },\n        {\n          PidFile: CorePidFilePath,\n          StopOutputKeyword: CoreStopOutputKeyword,\n          Env: getKernelRuntimeEnv(isAlpha),\n        },\n      ).catch((e) => reject(e))\n    })\n  }\n\n  const onCoreStarted = async (pid: number) => {\n    corePid.value = pid\n    running.value = true\n    needRestart.value = false\n    isCoreStartedByThisInstance = true\n    coreStoppedPromise = new Promise((r) => (coreStoppedResolver = r))\n\n    initWebsocket()\n    await Promise.all([refreshConfig(), refreshProviderProxies()])\n\n    if (appSettingsStore.app.autoSetSystemProxy) {\n      await envStore.setSystemProxy().catch((err) => message.error(err))\n    }\n    await envStore.updateSystemProxyStatus()\n\n    await pluginsStore.onCoreStartedTrigger()\n  }\n\n  const onCoreStopped = async () => {\n    if (!isCoreStartedByThisInstance) {\n      await RemoveFile(CorePidFilePath)\n    }\n\n    corePid.value = -1\n    running.value = false\n    needRestart.value = false\n\n    destroyWebsocket()\n\n    await envStore.updateSystemProxyStatus()\n    if (envStore.systemProxy) {\n      await envStore.clearSystemProxy()\n    }\n    await pluginsStore.onCoreStoppedTrigger()\n\n    coreStoppedResolver(null)\n  }\n\n  const startCore = async (_profile?: IProfile) => {\n    if (running.value) throw 'The core is already running'\n\n    logsStore.clearKernelLog()\n\n    const { profile: profileID, branch } = appSettingsStore.app.kernel\n    const profile = _profile || profilesStore.getProfileById(profileID)\n    if (!profile) throw 'Choose a profile first'\n\n    if (!_profile) {\n      runtimeProfile = undefined\n    }\n\n    starting.value = true\n    try {\n      await generateConfigFile(profile, (config) =>\n        pluginsStore.onBeforeCoreStartTrigger(config, profile),\n      )\n      const isAlpha = branch === Branch.Alpha\n      const pid = await runCoreProcess(isAlpha)\n      pid && (await onCoreStarted(pid))\n    } finally {\n      starting.value = false\n    }\n  }\n\n  const stopCore = async () => {\n    if (!running.value) throw 'The core is not running'\n\n    stopping.value = true\n    try {\n      await pluginsStore.onBeforeCoreStopTrigger()\n      await KillProcess(corePid.value)\n      await (isCoreStartedByThisInstance ? coreStoppedPromise : onCoreStopped())\n    } finally {\n      stopping.value = false\n    }\n  }\n\n  const restartCore = async (cleanupTask?: () => Promise<any>, keepRuntimeProfile = false) => {\n    restarting.value = true\n    try {\n      await stopCore()\n      await cleanupTask?.()\n      await startCore(keepRuntimeProfile ? runtimeProfile : undefined)\n    } finally {\n      needRestart.value = false\n      restarting.value = false\n    }\n  }\n\n  const getProxyPort = ():\n    | {\n        port: number\n        proxyType: ProxyType\n      }\n    | undefined => {\n    const { port, 'socks-port': socksPort, 'mixed-port': mixedPort } = config.value\n\n    if (mixedPort) {\n      return {\n        port: mixedPort,\n        proxyType: 'mixed',\n      }\n    }\n    if (port) {\n      return {\n        port,\n        proxyType: 'http',\n      }\n    }\n    if (socksPort) {\n      return {\n        port: socksPort,\n        proxyType: 'socks',\n      }\n    }\n    return undefined\n  }\n\n  eventBus.on('profileChange', ({ id }) => {\n    if (running.value && id === appSettingsStore.app.kernel.profile) {\n      needRestart.value = true\n    }\n  })\n\n  eventBus.on('subscriptionChange', ({ id }) => {\n    if (running.value && profilesStore.currentProfile) {\n      const inUse = profilesStore.currentProfile.outbounds.some(({ outbounds }) =>\n        outbounds.some((outbound) => outbound.type === 'Subscription' && outbound.id === id),\n      )\n      if (inUse) {\n        needRestart.value = true\n      }\n    }\n  })\n\n  eventBus.on('subscriptionsChange', () => {\n    if (running.value && profilesStore.currentProfile) {\n      const enabledSubs = subscribesStore.subscribes.flatMap((v) => (v.disabled ? [] : v.id))\n      const inUse = profilesStore.currentProfile.outbounds.some(({ outbounds }) =>\n        outbounds.some(\n          (outbound) => outbound.type === 'Subscription' && enabledSubs.includes(outbound.id),\n        ),\n      )\n      if (inUse) {\n        needRestart.value = true\n      }\n    }\n  })\n\n  const collectRulesetIDs = () => {\n    if (!profilesStore.currentProfile) return []\n    const l1 = profilesStore.currentProfile.route.rule_set.flatMap((ruleset) =>\n      ruleset.type === RulesetType.Local ? ruleset.path : [],\n    )\n    return l1\n  }\n\n  eventBus.on('rulesetChange', ({ id }) => {\n    if (running.value && profilesStore.currentProfile) {\n      const inUse = profilesStore.currentProfile.route.rule_set.some(\n        (ruleset) => ruleset.type === RulesetType.Local && ruleset.path === id,\n      )\n      if (inUse) {\n        needRestart.value = true\n      }\n    }\n  })\n\n  eventBus.on('rulesetsChange', () => {\n    if (running.value && profilesStore.currentProfile) {\n      const enabledRulesets = rulesetsStore.rulesets.flatMap((v) => (v.disabled ? [] : v.id))\n      const inUse = collectRulesetIDs().some((v) => enabledRulesets.includes(v))\n      if (inUse) {\n        needRestart.value = true\n      }\n    }\n  })\n\n  watch(needRestart, (v) => {\n    if (v && appSettingsStore.app.autoRestartKernel) {\n      restartCore()\n    }\n  })\n\n  const watchSources = computed(() => {\n    const source = [config.value.mode, config.value.tun.enable]\n    if (!appSettingsStore.app.addGroupToMenu) return source.join('')\n\n    const { unAvailable, sortByDelay } = appSettingsStore.app.kernel\n\n    const proxySignature = Object.values(proxies.value)\n      .map((group) => group.name + group.now)\n      .sort()\n      .join()\n\n    return source.concat([proxySignature, unAvailable, sortByDelay]).join('')\n  })\n\n  watch([watchSources, running], updateTrayAndMenus)\n\n  return {\n    startCore,\n    stopCore,\n    restartCore,\n    initCoreState,\n    pid: corePid,\n    running,\n    starting,\n    stopping,\n    restarting,\n    needRestart,\n    coreStateLoading,\n    config,\n    proxies,\n    refreshConfig,\n    updateConfig,\n    refreshProviderProxies,\n    getProxyPort,\n\n    onLogs,\n    onMemory,\n    onTraffic,\n    onConnections,\n  }\n})\n"
  },
  {
    "path": "frontend/src/stores/logs.ts",
    "content": "import { defineStore } from 'pinia'\nimport { computed, ref } from 'vue'\n\ninterface TaskLogRecord<T = any> {\n  name: string\n  startTime: number\n  endTime: number\n  result: T\n}\n\nexport const useLogsStore = defineStore('logs', () => {\n  const kernelLogs = ref<string[]>([])\n  const scheduledtasksLogs = ref<TaskLogRecord[]>([])\n\n  const recordKernelLog = (msg: string) => {\n    kernelLogs.value.unshift(msg)\n  }\n\n  const recordScheduledTasksLog = (log: TaskLogRecord) => scheduledtasksLogs.value.unshift(log)\n\n  const isTasksLogEmpty = computed(() => scheduledtasksLogs.value.length === 0)\n\n  const isEmpty = computed(() => kernelLogs.value.length === 0)\n\n  const clearKernelLog = () => kernelLogs.value.splice(0)\n\n  return {\n    recordKernelLog,\n    clearKernelLog,\n    kernelLogs,\n    isEmpty,\n    scheduledtasksLogs,\n    isTasksLogEmpty,\n    recordScheduledTasksLog,\n  }\n})\n"
  },
  {
    "path": "frontend/src/stores/plugins.ts",
    "content": "import { defineStore } from 'pinia'\nimport { computed, ref, watch } from 'vue'\nimport { parse } from 'yaml'\n\nimport { HttpGet, ReadFile, RemoveFile, WriteFile } from '@/bridge'\nimport { PluginHubFilePath, PluginsFilePath } from '@/constant/app'\nimport { PluginTrigger, PluginTriggerEvent } from '@/enums/app'\nimport { useAppSettingsStore } from '@/stores'\nimport {\n  ignoredError,\n  updateTrayAndMenus,\n  isNumber,\n  omitArray,\n  deepClone,\n  confirm,\n  asyncPool,\n  stringifyNoFolding,\n  readonly,\n} from '@/utils'\n\nimport type { Plugin, Subscription, TrayContent, MenuItem } from '@/types/app'\n\nconst PluginsCache: Recordable<{ plugin: Plugin; code: string }> = {}\n\nconst PluginsTriggerMap: {\n  [key in PluginTrigger]: {\n    fnName: PluginTriggerEvent\n    observers: string[]\n  }\n} = {\n  [PluginTrigger.OnManual]: {\n    fnName: PluginTriggerEvent.OnManual,\n    observers: [],\n  },\n  [PluginTrigger.OnTrayUpdate]: {\n    fnName: PluginTriggerEvent.OnTrayUpdate,\n    observers: [],\n  },\n  [PluginTrigger.OnSubscribe]: {\n    fnName: PluginTriggerEvent.OnSubscribe,\n    observers: [],\n  },\n  [PluginTrigger.OnGenerate]: {\n    fnName: PluginTriggerEvent.OnGenerate,\n    observers: [],\n  },\n  [PluginTrigger.OnStartup]: {\n    fnName: PluginTriggerEvent.OnStartup,\n    observers: [],\n  },\n  [PluginTrigger.OnShutdown]: {\n    fnName: PluginTriggerEvent.OnShutdown,\n    observers: [],\n  },\n  [PluginTrigger.OnReady]: {\n    fnName: PluginTriggerEvent.OnReady,\n    observers: [],\n  },\n  [PluginTrigger.OnReload]: {\n    fnName: PluginTriggerEvent.OnReload,\n    observers: [],\n  },\n  [PluginTrigger.OnCoreStarted]: {\n    fnName: PluginTriggerEvent.OnCoreStarted,\n    observers: [],\n  },\n  [PluginTrigger.OnCoreStopped]: {\n    fnName: PluginTriggerEvent.OnCoreStopped,\n    observers: [],\n  },\n  [PluginTrigger.OnBeforeCoreStart]: {\n    fnName: PluginTriggerEvent.OnBeforeCoreStart,\n    observers: [],\n  },\n  [PluginTrigger.OnBeforeCoreStop]: {\n    fnName: PluginTriggerEvent.OnBeforeCoreStop,\n    observers: [],\n  },\n}\n\nexport const usePluginsStore = defineStore('plugins', () => {\n  const appSettingsStore = useAppSettingsStore()\n\n  const plugins = ref<Plugin[]>([])\n  const pluginHub = ref<Plugin[]>([])\n\n  const setupPlugins = async () => {\n    const data = await ignoredError(ReadFile, PluginsFilePath)\n    data && (plugins.value = parse(data))\n\n    const list = await ignoredError(ReadFile, PluginHubFilePath)\n    list && (pluginHub.value = JSON.parse(list))\n\n    for (const plugin of plugins.value) {\n      const { id, triggers, path } = plugin\n      const code = await ignoredError(ReadFile, path)\n      if (code) {\n        PluginsCache[id] = { plugin, code }\n        triggers.forEach((trigger) => {\n          PluginsTriggerMap[trigger].observers.push(id)\n        })\n      }\n    }\n  }\n\n  const getPluginMetadata = (id: string) => {\n    const lastConfiguration: Recordable = { time: 0, data: undefined }\n    const buildConfiguration = (plugin: Plugin) => {\n      const now = performance.now()\n      if (lastConfiguration.data && now - lastConfiguration.time < 1000) {\n        return lastConfiguration.data\n      }\n\n      const configuration: Recordable = {}\n      for (const { key, value } of plugin.configuration) {\n        configuration[key] = value\n      }\n\n      const userSettings = appSettingsStore.app.pluginSettings[plugin.id]\n      if (userSettings) {\n        for (const key in userSettings) {\n          configuration[key] = userSettings[key]\n        }\n      }\n\n      lastConfiguration.time = now\n      lastConfiguration.data = configuration\n      return configuration\n    }\n\n    const lastPlugin: { time: number; data: Plugin | undefined } = { time: 0, data: undefined }\n    const getPlugin = () => {\n      const now = performance.now()\n      if (lastPlugin.data && now - lastPlugin.time < 1000) {\n        return lastPlugin.data\n      }\n      const cache = PluginsCache[id]\n      if (!cache) throw new Error()\n\n      lastPlugin.time = now\n      lastPlugin.data = cache.plugin\n      return cache.plugin\n    }\n\n    const proxy = new Proxy({} as Plugin & Recordable, {\n      get(_, p) {\n        const plugin = getPlugin()\n        if (typeof p === 'string' && p.startsWith('__v_')) {\n          return Reflect.get(plugin, p)\n        }\n\n        let value\n        if (Object.hasOwn(plugin, p)) {\n          value = Reflect.get(plugin, p)\n        } else {\n          const configuration = buildConfiguration(plugin)\n          value = Reflect.get(configuration, p)\n        }\n\n        if (p === 'status') return value\n\n        return readonly(value)\n      },\n\n      set(_, p, newValue) {\n        const plugin = getPlugin()\n\n        if (p === 'status') {\n          plugin.status = newValue\n          editPlugin(plugin.id, plugin)\n          return true\n        }\n\n        console.warn(`[${plugin.name}] Property \"${String(p)}\" is read-only.`)\n        return false\n      },\n\n      ownKeys() {\n        const plugin = getPlugin()\n        const configuration = buildConfiguration(plugin)\n        return [...Reflect.ownKeys(plugin), ...Reflect.ownKeys(configuration)]\n      },\n\n      getOwnPropertyDescriptor() {\n        return {\n          enumerable: true,\n          configurable: true,\n        }\n      },\n    })\n\n    return proxy\n  }\n\n  const isPluginUnavailable = (\n    cache: undefined | { plugin: Plugin; code: string },\n  ): cache is undefined => {\n    return (\n      !cache ||\n      !cache.plugin ||\n      cache.plugin.disabled ||\n      (cache.plugin.install && !cache.plugin.installed)\n    )\n  }\n\n  const reloadPlugin = async (plugin: Plugin, code = '', reloadTrigger = false) => {\n    const { path } = plugin\n    if (!code) {\n      code = await ReadFile(path)\n    }\n    PluginsCache[plugin.id] = { plugin, code }\n    reloadTrigger && updatePluginTrigger(plugin)\n  }\n\n  // FIXME: Plug-in execution order is wrong\n  const updatePluginTrigger = (plugin: Plugin, isUpdate = true) => {\n    const triggers = Object.keys(PluginsTriggerMap) as PluginTrigger[]\n    triggers.forEach((trigger) => {\n      PluginsTriggerMap[trigger].observers = PluginsTriggerMap[trigger].observers.filter(\n        (v) => v !== plugin.id,\n      )\n    })\n    if (isUpdate) {\n      plugin.triggers.forEach((trigger) => {\n        PluginsTriggerMap[trigger].observers.push(plugin.id)\n      })\n    }\n  }\n\n  const savePlugins = () => {\n    const p = omitArray(plugins.value, ['updating', 'loading', 'running'])\n    return WriteFile(PluginsFilePath, stringifyNoFolding(p))\n  }\n\n  const addPlugin = async (plugin: Plugin) => {\n    plugins.value.push(plugin)\n    try {\n      await _doUpdatePlugin(plugin)\n      await savePlugins()\n      updatePluginTrigger(plugin)\n    } catch (error) {\n      const idx = plugins.value.indexOf(plugin)\n      if (idx !== -1) {\n        plugins.value.splice(idx, 1)\n      }\n      throw error\n    }\n  }\n\n  const deletePlugin = async (id: string) => {\n    const idx = plugins.value.findIndex((v) => v.id === id)\n    if (idx === -1) return\n    const plugin = plugins.value.splice(idx, 1)[0]!\n    try {\n      await savePlugins()\n      delete PluginsCache[id]\n      updatePluginTrigger(plugin, false)\n    } catch (error) {\n      plugins.value.splice(idx, 0, plugin)\n      throw error\n    }\n    plugin.path.startsWith('data') && (await RemoveFile(plugin.path).catch((_) => {}))\n    // Remove configuration\n    if (appSettingsStore.app.pluginSettings[plugin.id]) {\n      if (await confirm('Tips', 'plugins.removeConfiguration').catch(() => 0)) {\n        delete appSettingsStore.app.pluginSettings[plugin.id]\n      }\n    }\n  }\n\n  const editPlugin = async (id: string, newPlugin: Plugin) => {\n    const idx = plugins.value.findIndex((v) => v.id === id)\n    if (idx === -1) return\n    const plugin = plugins.value.splice(idx, 1, newPlugin)[0]!\n    try {\n      await savePlugins()\n      if (PluginsCache[plugin.id]) {\n        PluginsCache[plugin.id]!.plugin = newPlugin\n      }\n      updatePluginTrigger(newPlugin)\n    } catch (error) {\n      plugins.value.splice(idx, 1, plugin)\n      throw error\n    }\n  }\n\n  const _doUpdatePlugin = async (plugin: Plugin) => {\n    const isFromPluginHub = plugin.id.startsWith('plugin-')\n    if (isFromPluginHub) {\n      const newPlugin = pluginHub.value.find((v) => v.id === plugin.id)\n      if (!newPlugin) throw 'Plugin not found. Please update the Plugin-Hub.'\n\n      const [major_now, minor_now, patch_now] = (plugin.version || '').substring(1).split('.')\n      const [major_new, minor_new, patch_new] = (newPlugin.version || '').substring(1).split('.')\n\n      if (major_now !== major_new) {\n        await editPlugin(plugin.id, deepClone(newPlugin))\n        const userSettigns = appSettingsStore.app.pluginSettings[plugin.id]\n        if (userSettigns) {\n          appSettingsStore.app.pluginSettings[plugin.id] = newPlugin.configuration.reduce(\n            (p, c) => {\n              const value_now = userSettigns[c.key]\n              const value_new = c.value\n              const type_now = Array.isArray(value_now) ? 'array' : typeof value_now\n              const type_new = Array.isArray(value_new) ? 'array' : typeof value_new\n              return {\n                ...p,\n                [c.key]: type_now === type_new ? value_now : value_new,\n              }\n            },\n            {},\n          )\n        }\n      } else if (minor_now !== minor_new || patch_now !== patch_new) {\n        plugin.version = newPlugin.version\n        await editPlugin(plugin.id, plugin)\n      }\n    }\n\n    let code = ''\n\n    if (plugin.type === 'File') {\n      code = await ReadFile(plugin.path).catch(() => '')\n    }\n\n    if (plugin.type === 'Http') {\n      const { body } = await HttpGet(plugin.url)\n      code = body\n    }\n\n    if (plugin.type !== 'File') {\n      await WriteFile(plugin.path, code)\n    }\n\n    PluginsCache[plugin.id] = { plugin, code }\n  }\n\n  const updatePlugin = async (id: string) => {\n    const plugin = plugins.value.find((v) => v.id === id)\n    if (!plugin) throw id + ' Not Found'\n    if (plugin.disabled) throw plugin.name + ' is Disabled'\n    try {\n      plugin.updating = true\n      await _doUpdatePlugin(plugin)\n      return `Plugin [${plugin.name}] updated successfully.`\n    } finally {\n      plugin.updating = false\n    }\n  }\n\n  const updatePlugins = async () => {\n    let needSave = false\n\n    const update = async (plugin: Plugin) => {\n      const result = { ok: true, id: plugin.id, name: plugin.name, result: '' }\n      try {\n        plugin.updating = true\n        await _doUpdatePlugin(plugin)\n        needSave = true\n        result.result = `Plugin [${plugin.name}] updated successfully.`\n      } catch (error: any) {\n        result.ok = false\n        result.result = `Failed to update plugin [${plugin.name}]. Reason: ${error.message || error}`\n      } finally {\n        plugin.updating = false\n      }\n      return result\n    }\n\n    const result = await asyncPool(\n      5,\n      plugins.value.filter((v) => !v.disabled),\n      update,\n    )\n\n    if (needSave) await savePlugins()\n\n    return result.flatMap((v) => (v.ok && v.value) || [])\n  }\n\n  const pluginHubLoading = ref(false)\n  const findPluginInHubById = (id: string) => pluginHub.value.find((v) => v.id === id)\n  const isDeprecated = (plugin: Plugin) => {\n    if (!plugin.id.startsWith('plugin-')) return false\n    return !findPluginInHubById(plugin.id)\n  }\n  const isDevVersion = (plugin: Plugin) => {\n    return plugin.version.startsWith('v0')\n  }\n  const hasNewPluginVersion = (plugin: Plugin) => {\n    const p = findPluginInHubById(plugin.id)\n    if (!p) return false\n    return p.version !== plugin.version\n  }\n  const updatePluginHub = async () => {\n    pluginHubLoading.value = true\n    try {\n      const { body: body1 } = await HttpGet<string>(\n        'https://raw.githubusercontent.com/GUI-for-Cores/Plugin-Hub/main/plugins/generic.json',\n      )\n      const { body: body2 } = await HttpGet<string>(\n        'https://raw.githubusercontent.com/GUI-for-Cores/Plugin-Hub/main/plugins/gfs.json',\n      )\n      pluginHub.value = [...JSON.parse(body1), ...JSON.parse(body2)]\n      await WriteFile(PluginHubFilePath, JSON.stringify(pluginHub.value))\n    } finally {\n      pluginHubLoading.value = false\n    }\n  }\n\n  const getPluginById = (id: string) => plugins.value.find((v) => v.id === id)\n\n  const getPluginCodefromCache = (id: string) => PluginsCache[id]?.code\n\n  const onSubscribeTrigger = async (proxies: Recordable[], subscription: Subscription) => {\n    const { fnName, observers } = PluginsTriggerMap[PluginTrigger.OnSubscribe]\n    if (observers.length === 0) return proxies\n\n    subscription = deepClone(subscription)\n\n    for (const observer of observers) {\n      const cache = PluginsCache[observer]\n\n      if (isPluginUnavailable(cache)) continue\n\n      const metadata = getPluginMetadata(observer)\n      try {\n        const fn = new window.AsyncFunction(\n          'Plugin',\n          'proxies',\n          'subscription',\n          `${cache.code}; return await ${fnName}(proxies, subscription)`,\n        )\n        proxies = await fn(metadata, proxies, subscription)\n      } catch (error: any) {\n        throw `${cache.plugin.name} : ` + (error.message || error)\n      }\n\n      if (!Array.isArray(proxies)) {\n        throw `${cache.plugin.name} : Wrong result`\n      }\n    }\n\n    return proxies\n  }\n\n  const noParamsTrigger = async (trigger: PluginTrigger, interruptOnError = false) => {\n    const { fnName, observers } = PluginsTriggerMap[trigger]\n    if (observers.length === 0) return\n\n    for (const observer of observers) {\n      const cache = PluginsCache[observer]\n\n      if (isPluginUnavailable(cache)) continue\n\n      const metadata = getPluginMetadata(observer)\n      try {\n        const fn = new window.AsyncFunction('Plugin', `${cache.code}; return await ${fnName}()`)\n        const exitCode = await fn(metadata)\n        if (isNumber(exitCode) && exitCode !== cache.plugin.status) {\n          cache.plugin.status = exitCode\n          editPlugin(cache.plugin.id, cache.plugin)\n        }\n      } catch (error: any) {\n        const msg = `${cache.plugin.name} : ` + (error.message || error)\n        if (interruptOnError) {\n          throw msg\n        }\n        console.error(msg)\n      }\n    }\n  }\n\n  const onGenerateTrigger = async (config: Recordable, profile: IProfile) => {\n    const { fnName, observers } = PluginsTriggerMap[PluginTrigger.OnGenerate]\n    if (observers.length === 0) return config\n\n    profile = deepClone(profile)\n\n    for (const observer of observers) {\n      const cache = PluginsCache[observer]\n\n      if (isPluginUnavailable(cache)) continue\n\n      const metadata = getPluginMetadata(observer)\n      try {\n        const fn = new window.AsyncFunction(\n          'Plugin',\n          'config',\n          'profile',\n          `${cache.code}; return await ${fnName}(config, profile)`,\n        )\n        config = await fn(metadata, config, profile)\n      } catch (error: any) {\n        throw `${cache.plugin.name} : ` + (error.message || error)\n      }\n\n      if (!config) throw `${cache.plugin.name} : Wrong result`\n    }\n\n    return config\n  }\n\n  const onBeforeCoreStartTrigger = async (params: Recordable, profile: IProfile) => {\n    const { fnName, observers } = PluginsTriggerMap[PluginTrigger.OnBeforeCoreStart]\n    if (observers.length === 0) return params\n\n    profile = deepClone(profile)\n\n    for (const observer of observers) {\n      const cache = PluginsCache[observer]\n\n      if (isPluginUnavailable(cache)) continue\n\n      const metadata = getPluginMetadata(observer)\n      try {\n        const fn = new window.AsyncFunction(\n          'Plugin',\n          'config',\n          'profile',\n          `${cache.code}; return await ${fnName}(config, profile)`,\n        )\n        params = await fn(metadata, params, profile)\n      } catch (error: any) {\n        throw `${cache.plugin.name} : ` + (error.message || error)\n      }\n\n      if (!params) throw `${cache.plugin.name} : Wrong result`\n    }\n\n    return params\n  }\n\n  const manualTrigger = async (id: string, event: PluginTriggerEvent, ...args: any[]) => {\n    const plugin = getPluginById(id)\n    if (!plugin) throw id + ' Not Found'\n    const cache = PluginsCache[plugin.id]\n    if (!cache) throw `${plugin.name} is Missing source code`\n    if (cache.plugin.disabled) throw `${plugin.name} is Disabled`\n    const metadata = getPluginMetadata(id)\n    args = deepClone(args)\n    try {\n      const fn = new window.AsyncFunction(\n        'Plugin',\n        '...args',\n        `${cache.code}; return await ${event}(...args)`,\n      )\n\n      const exitCode = await fn(metadata, ...args)\n      if (isNumber(exitCode) && exitCode !== plugin.status) {\n        plugin.status = exitCode\n        editPlugin(id, plugin)\n      }\n      return exitCode\n    } catch (error: any) {\n      throw `${cache.plugin.name} : ` + (error.message || error)\n    }\n  }\n\n  const onTrayUpdateTrigger = async (tray: TrayContent, menus: MenuItem[]) => {\n    const { fnName, observers } = PluginsTriggerMap[PluginTrigger.OnTrayUpdate]\n    if (observers.length === 0) return [tray, menus] as const\n\n    let finalTray = tray\n    let finalMenus = menus\n    for (const observer of observers) {\n      const cache = PluginsCache[observer]\n\n      if (isPluginUnavailable(cache)) continue\n\n      const metadata = getPluginMetadata(observer)\n      try {\n        const fn = new window.AsyncFunction(\n          'Plugin',\n          'tray',\n          'menus',\n          `${cache.code}; return await ${fnName}(tray, menus)`,\n        )\n        const { tray, menus } = await fn(metadata, finalTray, finalMenus)\n        finalTray = tray\n        finalMenus = menus\n      } catch (error: any) {\n        throw `${cache.plugin.name} : ` + (error.message || error)\n      }\n    }\n\n    return [finalTray, finalMenus] as const\n  }\n\n  const _watchDisabled = computed(() =>\n    plugins.value\n      .map((v) => v.disabled)\n      .sort()\n      .join(),\n  )\n\n  const _watchMenus = computed(() =>\n    plugins.value\n      .map((v) => Object.entries(v.menus).map((v) => v[0] + v[1]))\n      .sort()\n      .join(),\n  )\n\n  watch([_watchMenus, _watchDisabled], () => {\n    if (appSettingsStore.app.addPluginToMenu) {\n      updateTrayAndMenus()\n    }\n  })\n\n  return {\n    plugins,\n    setupPlugins,\n    savePlugins,\n    addPlugin,\n    editPlugin,\n    deletePlugin,\n    updatePlugin,\n    updatePlugins,\n    getPluginById,\n    reloadPlugin,\n    onTrayUpdateTrigger,\n    onSubscribeTrigger,\n    onGenerateTrigger,\n    onStartupTrigger: () => noParamsTrigger(PluginTrigger.OnStartup),\n    onShutdownTrigger: () => noParamsTrigger(PluginTrigger.OnShutdown, true),\n    onReadyTrigger: () => noParamsTrigger(PluginTrigger.OnReady),\n    onReloadTrigger: () => noParamsTrigger(PluginTrigger.OnReload, true),\n    onCoreStartedTrigger: () => noParamsTrigger(PluginTrigger.OnCoreStarted),\n    onCoreStoppedTrigger: () => noParamsTrigger(PluginTrigger.OnCoreStopped),\n    onBeforeCoreStopTrigger: () => noParamsTrigger(PluginTrigger.OnBeforeCoreStop, true),\n    onBeforeCoreStartTrigger,\n    manualTrigger,\n    updatePluginTrigger,\n    getPluginCodefromCache,\n    getPluginMetadata,\n\n    pluginHub,\n    pluginHubLoading,\n    updatePluginHub,\n    hasNewPluginVersion,\n    findPluginInHubById,\n    isDeprecated,\n    isDevVersion,\n  }\n})\n"
  },
  {
    "path": "frontend/src/stores/profiles.ts",
    "content": "import { defineStore } from 'pinia'\nimport { computed, ref } from 'vue'\nimport { parse } from 'yaml'\n\nimport { ReadFile, WriteFile } from '@/bridge'\nimport { ProfilesFilePath } from '@/constant/app'\nimport * as Defaults from '@/constant/profile'\nimport { useAppSettingsStore } from '@/stores'\nimport { ignoredError, eventBus, stringifyNoFolding, migrateProfiles, sampleID } from '@/utils'\n\nexport const useProfilesStore = defineStore('profiles', () => {\n  const appSettingsStore = useAppSettingsStore()\n\n  const profiles = ref<IProfile[]>([])\n  const currentProfile = computed(() => getProfileById(appSettingsStore.app.kernel.profile))\n\n  const setupProfiles = async () => {\n    const data = await ignoredError(ReadFile, ProfilesFilePath)\n    data && (profiles.value = parse(data))\n\n    await migrateProfiles(profiles.value, saveProfiles)\n  }\n\n  const saveProfiles = () => {\n    return WriteFile(ProfilesFilePath, stringifyNoFolding(profiles.value))\n  }\n\n  const addProfile = async (p: IProfile) => {\n    profiles.value.push(p)\n    try {\n      await saveProfiles()\n    } catch (error) {\n      const idx = profiles.value.indexOf(p)\n      if (idx !== -1) {\n        profiles.value.splice(idx, 1)\n      }\n      throw error\n    }\n  }\n\n  const deleteProfile = async (id: string) => {\n    const idx = profiles.value.findIndex((v) => v.id === id)\n    if (idx === -1) return\n    const backup = profiles.value.splice(idx, 1)[0]!\n    try {\n      await saveProfiles()\n    } catch (error) {\n      profiles.value.splice(idx, 0, backup)\n      throw error\n    }\n\n    eventBus.emit('profileChange', { id })\n  }\n\n  const editProfile = async (id: string, p: IProfile) => {\n    const idx = profiles.value.findIndex((v) => v.id === id)\n    if (idx === -1) return\n    const backup = profiles.value.splice(idx, 1, p)[0]!\n    try {\n      await saveProfiles()\n    } catch (error) {\n      profiles.value.splice(idx, 1, backup)\n      throw error\n    }\n\n    eventBus.emit('profileChange', { id })\n  }\n\n  const getProfileById = (id: string) => profiles.value.find((v) => v.id === id)\n\n  const getProfileTemplate = (name = ''): IProfile => {\n    return {\n      id: sampleID(),\n      name: name,\n      log: Defaults.DefaultLog(),\n      experimental: Defaults.DefaultExperimental(),\n      inbounds: Defaults.DefaultInbounds(),\n      outbounds: Defaults.DefaultOutbounds(),\n      route: Defaults.DefaultRoute(),\n      dns: Defaults.DefaultDns(),\n      mixin: Defaults.DefaultMixin(),\n      script: Defaults.DefaultScript(),\n    }\n  }\n\n  return {\n    profiles,\n    currentProfile,\n    setupProfiles,\n    saveProfiles,\n    addProfile,\n    editProfile,\n    deleteProfile,\n    getProfileById,\n    getProfileTemplate,\n  }\n})\n"
  },
  {
    "path": "frontend/src/stores/rulesets.ts",
    "content": "import { defineStore } from 'pinia'\nimport { ref } from 'vue'\nimport { parse } from 'yaml'\n\nimport { ReadFile, WriteFile, CopyFile, Download, HttpGet } from '@/bridge'\nimport { RulesetHubFilePath, RulesetsFilePath } from '@/constant/app'\nimport { EmptyRuleSet } from '@/constant/kernel'\nimport { RulesetFormat } from '@/enums/kernel'\nimport {\n  asyncPool,\n  stringifyNoFolding,\n  eventBus,\n  ignoredError,\n  isValidRulesJson,\n  omitArray,\n} from '@/utils'\n\nexport interface RuleSet {\n  id: string\n  tag: string\n  updateTime: number\n  disabled: boolean\n  type: 'Http' | 'File' | 'Manual'\n  format: RulesetFormat\n  path: string\n  url: string\n  count: number\n  // Not Config\n  updating?: boolean\n}\n\nexport interface RuleSetHub {\n  geosite: string\n  geoip: string\n  list: { name: string; type: 'geosite' | 'geoip'; description: string; count: number }[]\n}\n\nexport const useRulesetsStore = defineStore('rulesets', () => {\n  const rulesets = ref<RuleSet[]>([])\n  const rulesetHub = ref<RuleSetHub>({ geosite: '', geoip: '', list: [] })\n\n  const setupRulesets = async () => {\n    const data = await ignoredError(ReadFile, RulesetsFilePath)\n    data && (rulesets.value = parse(data))\n\n    const list = await ignoredError(ReadFile, RulesetHubFilePath)\n    list && (rulesetHub.value = JSON.parse(list))\n  }\n\n  const saveRulesets = () => {\n    const r = omitArray(rulesets.value, ['updating'])\n    return WriteFile(RulesetsFilePath, stringifyNoFolding(r))\n  }\n\n  const addRuleset = async (r: RuleSet) => {\n    rulesets.value.push(r)\n    try {\n      await saveRulesets()\n    } catch (error) {\n      const idx = rulesets.value.indexOf(r)\n      if (idx !== -1) {\n        rulesets.value.splice(idx, 1)\n      }\n      throw error\n    }\n  }\n\n  const deleteRuleset = async (id: string) => {\n    const idx = rulesets.value.findIndex((v) => v.id === id)\n    if (idx === -1) return\n    const backup = rulesets.value.splice(idx, 1)[0]!\n    try {\n      await saveRulesets()\n    } catch (error) {\n      rulesets.value.splice(idx, 0, backup)\n      throw error\n    }\n\n    eventBus.emit('rulesetChange', { id })\n  }\n\n  const editRuleset = async (id: string, r: RuleSet) => {\n    const idx = rulesets.value.findIndex((v) => v.id === id)\n    if (idx === -1) return\n    const backup = rulesets.value.splice(idx, 1, r)[0]!\n    try {\n      await saveRulesets()\n    } catch (error) {\n      rulesets.value.splice(idx, 1, backup)\n      throw error\n    }\n\n    eventBus.emit('rulesetChange', { id })\n  }\n\n  const _doUpdateRuleset = async (r: RuleSet) => {\n    if (r.format === RulesetFormat.Source) {\n      let body = ''\n      let isExist = true\n\n      if (r.type === 'File') {\n        body = await ReadFile(r.url)\n      } else if (r.type === 'Http') {\n        const { body: b } = await HttpGet(r.url)\n        body = b\n        if (typeof body !== 'string') {\n          body = JSON.stringify(body)\n        }\n      } else if (r.type === 'Manual') {\n        body = await ReadFile(r.path).catch(() => '')\n        if (!body) {\n          body = JSON.stringify(EmptyRuleSet)\n          isExist = false\n        }\n      }\n\n      if (!isValidRulesJson(body)) {\n        throw 'Not a valid ruleset data'\n      }\n\n      const ruleset = JSON.parse(body)\n\n      r.count = ruleset.rules.reduce(\n        (p: number, c: string[]) =>\n          Object.values(c).reduce(\n            (p, c: string[] | string) => (Array.isArray(c) ? p + c.length : p + 1),\n            0,\n          ) + p,\n        0,\n      )\n\n      if (\n        (['Http', 'File'].includes(r.type) && r.url !== r.path) ||\n        (r.type === 'Manual' && !isExist)\n      ) {\n        await WriteFile(r.path, JSON.stringify(ruleset, null, 2))\n      }\n    }\n\n    if (r.format === RulesetFormat.Binary) {\n      if (r.type === 'File' && r.url !== r.path) {\n        await CopyFile(r.url, r.path)\n      } else if (r.type === 'Http') {\n        await Download(r.url, r.path)\n      }\n    }\n\n    r.updateTime = Date.now()\n  }\n\n  const updateRuleset = async (id: string) => {\n    const r = rulesets.value.find((v) => v.id === id)\n    if (!r) throw id + ' Not Found'\n    if (r.disabled) throw r.tag + ' Disabled'\n    try {\n      r.updating = true\n      await _doUpdateRuleset(r)\n      await saveRulesets()\n    } finally {\n      r.updating = false\n    }\n\n    eventBus.emit('rulesetChange', { id })\n\n    return `Ruleset [${r.tag}] updated successfully.`\n  }\n\n  const updateRulesets = async () => {\n    let needSave = false\n\n    const update = async (r: RuleSet) => {\n      const result = { ok: true, id: r.id, name: r.tag, result: '' }\n      try {\n        r.updating = true\n        await _doUpdateRuleset(r)\n        needSave = true\n        result.result = `Rule-Set [${r.tag}] updated successfully.`\n      } catch (error: any) {\n        result.ok = false\n        result.result = `Failed to update rule-set [${r.tag}]. Reason: ${error.message || error}`\n      } finally {\n        r.updating = false\n      }\n      return result\n    }\n\n    const result = await asyncPool(\n      5,\n      rulesets.value.filter((v) => !v.disabled),\n      update,\n    )\n\n    if (needSave) await saveRulesets()\n\n    eventBus.emit('rulesetsChange', undefined)\n\n    return result.flatMap((v) => (v.ok && v.value) || [])\n  }\n\n  const rulesetHubLoading = ref(false)\n  const updateRulesetHub = async () => {\n    rulesetHubLoading.value = true\n    try {\n      const { body } = await HttpGet<string>(\n        'https://github.com/GUI-for-Cores/Ruleset-Hub/releases/download/latest/sing-full.json',\n      )\n      rulesetHub.value = JSON.parse(body)\n      await WriteFile(RulesetHubFilePath, body)\n    } finally {\n      rulesetHubLoading.value = false\n    }\n  }\n\n  const getRulesetById = (id: string) => rulesets.value.find((v) => v.id === id)\n\n  return {\n    rulesets,\n    setupRulesets,\n    saveRulesets,\n    addRuleset,\n    editRuleset,\n    deleteRuleset,\n    updateRuleset,\n    updateRulesets,\n    getRulesetById,\n\n    rulesetHub,\n    rulesetHubLoading,\n    updateRulesetHub,\n  }\n})\n"
  },
  {
    "path": "frontend/src/stores/scheduledtasks.ts",
    "content": "import { Cron } from 'croner'\nimport { defineStore } from 'pinia'\nimport { ref } from 'vue'\nimport { parse } from 'yaml'\n\nimport { ReadFile, WriteFile, Notify } from '@/bridge'\nimport { ScheduledTasksFilePath } from '@/constant/app'\nimport { ScheduledTasksType, PluginTriggerEvent } from '@/enums/app'\nimport { useSubscribesStore, useRulesetsStore, usePluginsStore, useLogsStore } from '@/stores'\nimport { ignoredError, stringifyNoFolding } from '@/utils'\n\nimport type { ScheduledTask } from '@/types/app'\n\nexport const useScheduledTasksStore = defineStore('scheduledtasks', () => {\n  const scheduledtasks = ref<ScheduledTask[]>([])\n  const cronJobsMap: Recordable<Cron> = {}\n\n  const setupScheduledTasks = async () => {\n    const data = await ignoredError(ReadFile, ScheduledTasksFilePath)\n    data && (scheduledtasks.value = parse(data))\n\n    scheduledtasks.value.forEach(async ({ disabled, cron, id }) => {\n      if (!disabled) {\n        cronJobsMap[id] = new Cron(cron, () => runScheduledTask(id))\n      }\n    })\n  }\n\n  const runScheduledTask = async (id: string) => {\n    const task = getScheduledTaskById(id)\n    if (!task) return\n\n    const logsStore = useLogsStore()\n\n    task.lastTime = Date.now()\n\n    const startTime = Date.now()\n    const result = await getTaskFn(task)()\n\n    if (task.notification) {\n      const successes = result.filter((v) => v.ok).length\n      const failures = result.length - successes\n      const details = result.flatMap((v) => v.result).join('\\n')\n      const content = `Successes: ${successes}; Failures: ${failures}. \\n\\n${details}`\n      Notify(task.name, content)\n    }\n\n    logsStore.recordScheduledTasksLog({\n      name: task.name,\n      startTime,\n      endTime: Date.now(),\n      result: result,\n    })\n\n    await editScheduledTask(id, task)\n  }\n\n  const withOutput = <T>(list: string[], fn: (id: string) => Promise<T>) => {\n    return async () => {\n      const output: { ok: boolean; result: T }[] = []\n      for (const id of list) {\n        try {\n          const result = await fn(id)\n          if (Array.isArray(result)) {\n            output.push(...result)\n          } else {\n            output.push({ ok: true, result })\n          }\n        } catch (error: any) {\n          output.push({ ok: false, result: error.message || error })\n        }\n      }\n      return output\n    }\n  }\n\n  const getTaskFn = (task: ScheduledTask) => {\n    switch (task.type) {\n      case ScheduledTasksType.UpdateSubscription: {\n        const subscribesStore = useSubscribesStore()\n        return withOutput(task.subscriptions, subscribesStore.updateSubscribe)\n      }\n      case ScheduledTasksType.UpdateRuleset: {\n        const rulesetsStore = useRulesetsStore()\n        return withOutput(task.rulesets, rulesetsStore.updateRuleset)\n      }\n      case ScheduledTasksType.UpdatePlugin: {\n        const pluginsStores = usePluginsStore()\n        return withOutput(task.plugins, pluginsStores.updatePlugin)\n      }\n      case ScheduledTasksType.UpdateAllSubscription: {\n        const subscribesStore = useSubscribesStore()\n        return withOutput(['0'], () => subscribesStore.updateSubscribes())\n      }\n      case ScheduledTasksType.UpdateAllRuleset: {\n        const rulesetsStore = useRulesetsStore()\n        return withOutput(['1'], () => rulesetsStore.updateRulesets())\n      }\n      case ScheduledTasksType.UpdateAllPlugin: {\n        const pluginsStores = usePluginsStore()\n        return withOutput(['2'], () => pluginsStores.updatePlugins())\n      }\n      case ScheduledTasksType.RunPlugin: {\n        const pluginsStores = usePluginsStore()\n        return withOutput(task.plugins, async (id: string) =>\n          pluginsStores.manualTrigger(id, PluginTriggerEvent.OnTask),\n        )\n      }\n      case ScheduledTasksType.RunScript: {\n        return withOutput([task.script], (script: string) => new window.AsyncFunction(script)())\n      }\n    }\n  }\n\n  const saveScheduledTasks = () => {\n    return WriteFile(ScheduledTasksFilePath, stringifyNoFolding(scheduledtasks.value))\n  }\n\n  const addScheduledTask = async (s: ScheduledTask) => {\n    scheduledtasks.value.push(s)\n    try {\n      cronJobsMap[s.id] = new Cron(s.cron, () => runScheduledTask(s.id))\n      await saveScheduledTasks()\n    } catch (error) {\n      cronJobsMap[s.id]?.stop()\n      delete cronJobsMap[s.id]\n      const idx = scheduledtasks.value.indexOf(s)\n      if (idx !== -1) {\n        scheduledtasks.value.splice(idx, 1)\n      }\n      throw error\n    }\n  }\n\n  const deleteScheduledTask = async (id: string) => {\n    const idx = scheduledtasks.value.findIndex((v) => v.id === id)\n    if (idx === -1) return\n    const backup = scheduledtasks.value.splice(idx, 1)[0]!\n    try {\n      await saveScheduledTasks()\n      cronJobsMap[id]?.stop()\n      delete cronJobsMap[id]\n    } catch (error) {\n      scheduledtasks.value.splice(idx, 0, backup)\n      throw error\n    }\n  }\n\n  const editScheduledTask = async (id: string, s: ScheduledTask) => {\n    const idx = scheduledtasks.value.findIndex((v) => v.id === id)\n    if (idx === -1) return\n    const backup = scheduledtasks.value.splice(idx, 1, s)[0]!\n    try {\n      await saveScheduledTasks()\n      cronJobsMap[id]?.stop()\n      if (s.disabled) {\n        delete cronJobsMap[id]\n      } else {\n        cronJobsMap[id] = new Cron(s.cron, () => runScheduledTask(id))\n      }\n    } catch (error) {\n      scheduledtasks.value.splice(idx, 1, backup)\n      throw error\n    }\n  }\n\n  const getScheduledTaskById = (id: string) => scheduledtasks.value.find((v) => v.id === id)\n\n  return {\n    scheduledtasks,\n    setupScheduledTasks,\n    saveScheduledTasks,\n    addScheduledTask,\n    editScheduledTask,\n    deleteScheduledTask,\n    getScheduledTaskById,\n    getTaskFn,\n    runScheduledTask,\n  }\n})\n"
  },
  {
    "path": "frontend/src/stores/subscribes.ts",
    "content": "import { defineStore } from 'pinia'\nimport { ref } from 'vue'\nimport { parse } from 'yaml'\n\nimport { ReadFile, WriteFile, Requests } from '@/bridge'\nimport { DefaultSubscribeScript, SubscribesFilePath } from '@/constant/app'\nimport { DefaultExcludeProtocols } from '@/constant/kernel'\nimport { PluginTriggerEvent, RequestMethod } from '@/enums/app'\nimport { usePluginsStore } from '@/stores'\nimport {\n  sampleID,\n  isValidSubJson,\n  isValidSubYAML,\n  isValidBase64,\n  stringifyNoFolding,\n  ignoredError,\n  omitArray,\n  asyncPool,\n  eventBus,\n  buildSmartRegExp,\n} from '@/utils'\n\nimport type { Subscription } from '@/types/app'\n\nexport const useSubscribesStore = defineStore('subscribes', () => {\n  const subscribes = ref<Subscription[]>([])\n\n  const setupSubscribes = async () => {\n    const data = await ignoredError(ReadFile, SubscribesFilePath)\n    data && (subscribes.value = parse(data))\n  }\n\n  const saveSubscribes = () => {\n    const s = omitArray(subscribes.value, ['updating'])\n    return WriteFile(SubscribesFilePath, stringifyNoFolding(s))\n  }\n\n  const addSubscribe = async (s: Subscription) => {\n    subscribes.value.push(s)\n    try {\n      await saveSubscribes()\n    } catch (error) {\n      const idx = subscribes.value.indexOf(s)\n      if (idx !== -1) {\n        subscribes.value.splice(idx, 1)\n      }\n      throw error\n    }\n  }\n\n  const importSubscribe = async (name: string, url: string) => {\n    await addSubscribe(getSubscribeTemplate(name, { url }))\n  }\n\n  const deleteSubscribe = async (id: string) => {\n    const idx = subscribes.value.findIndex((v) => v.id === id)\n    if (idx === -1) return\n    const backup = subscribes.value.splice(idx, 1)[0]!\n    try {\n      await saveSubscribes()\n    } catch (error) {\n      subscribes.value.splice(idx, 0, backup)\n      throw error\n    }\n\n    eventBus.emit('subscriptionChange', { id })\n  }\n\n  const editSubscribe = async (id: string, s: Subscription) => {\n    const idx = subscribes.value.findIndex((v) => v.id === id)\n    if (idx === -1) return\n    const backup = subscribes.value.splice(idx, 1, s)[0]!\n    try {\n      await saveSubscribes()\n    } catch (error) {\n      subscribes.value.splice(idx, 1, backup)\n      throw error\n    }\n\n    eventBus.emit('subscriptionChange', { id })\n  }\n\n  const _doUpdateSub = async (s: Subscription) => {\n    const userInfo: Recordable = {}\n    let body = ''\n    let proxies: Record<string, any>[] = []\n\n    if (s.type === 'Manual') {\n      body = await ReadFile(s.path)\n    }\n\n    if (s.type === 'File') {\n      body = await ReadFile(s.url)\n    }\n\n    if (s.type === 'Http') {\n      const { headers: h, body: b } = await Requests({\n        method: s.requestMethod,\n        url: s.url,\n        headers: s.header.request,\n        autoTransformBody: false,\n        options: {\n          Insecure: s.inSecure,\n          Timeout: s.requestTimeout,\n        },\n      })\n      Object.assign(h, s.header.response)\n      if (h['Subscription-Userinfo']) {\n        ;(h['Subscription-Userinfo'] as string).split(/\\s*;\\s*/).forEach((part) => {\n          const [key, value] = part.split('=') as [string, string]\n          userInfo[key] = parseInt(value) || 0\n        })\n      }\n      body = b\n    }\n\n    if (isValidSubJson(body)) {\n      proxies = JSON.parse(body).outbounds\n    } else if (isValidSubYAML(body)) {\n      proxies = parse(body).proxies\n    } else if (isValidBase64(body)) {\n      proxies = [{ base64: body }]\n    } else if (s.type === 'Manual') {\n      proxies = JSON.parse(body)\n    } else {\n      throw 'Not a valid subscription data'\n    }\n\n    const pluginStore = usePluginsStore()\n\n    proxies = await pluginStore.onSubscribeTrigger(proxies, s)\n\n    if (proxies.some((proxy) => proxy.name && !proxy.tag) || proxies[0]?.base64) {\n      throw 'You need to install the [节点转换] plugin first'\n    }\n\n    if (s.type !== 'Manual') {\n      const r1 = s.include && buildSmartRegExp(s.include)\n      const r2 = s.exclude && buildSmartRegExp(s.exclude)\n      const r3 = s.includeProtocol && buildSmartRegExp(s.includeProtocol)\n      const r4 = s.excludeProtocol && buildSmartRegExp(s.excludeProtocol)\n\n      proxies = proxies.filter((v) => {\n        const flag1 = r1 ? r1.test(v.tag) : true\n        const flag2 = r2 ? r2.test(v.tag) : false\n        const flag3 = r3 ? r3.test(v.type) : true\n        const flag4 = r4 ? r4.test(v.type) : false\n        return flag1 && !flag2 && flag3 && !flag4\n      })\n\n      if (s.proxyPrefix) {\n        proxies.forEach((v) => {\n          v.tag = v.tag.startsWith(s.proxyPrefix) ? v.tag : s.proxyPrefix + v.tag\n        })\n      }\n    }\n\n    s.upload = userInfo.upload ?? 0\n    s.download = userInfo.download ?? 0\n    s.total = userInfo.total ?? 0\n    s.expire = userInfo.expire * 1000\n    s.updateTime = Date.now()\n    s.proxies = proxies.map(({ tag, type }) => {\n      // Keep the original ID value of the proxy unchanged\n      const id = s.proxies.find((v) => v.tag === tag)?.id || sampleID()\n      return { id, tag, type }\n    })\n\n    const fn = new window.AsyncFunction(\n      'proxies',\n      'subscription',\n      `${s.script}; return await ${PluginTriggerEvent.OnSubscribe}(proxies, subscription)`,\n    ) as (\n      proxies: Recordable[],\n      subscription: Subscription,\n    ) => Promise<{ proxies: Recordable[]; subscription: Subscription }>\n\n    const { proxies: _proxies, subscription } = await fn(proxies, s)\n\n    Object.assign(s, subscription)\n    s.proxies = _proxies.map(({ tag, type }) => {\n      // Keep the original ID value of the proxy unchanged\n      const id = s.proxies.find((v) => v.tag === tag)?.id || sampleID()\n      return { id, tag, type }\n    })\n\n    if (s.type === 'Http' || (s.type === 'File' && s.url !== s.path)) {\n      proxies = omitArray(_proxies, ['__id__', '__tmp__id__'])\n      await WriteFile(s.path, JSON.stringify(proxies, null, 2))\n    }\n  }\n\n  const updateSubscribe = async (id: string) => {\n    const s = subscribes.value.find((v) => v.id === id)\n    if (!s) throw id + ' Not Found'\n    if (s.disabled) throw s.name + ' Disabled'\n    try {\n      s.updating = true\n      await _doUpdateSub(s)\n      await saveSubscribes()\n    } catch (error: any) {\n      throw `Failed to update subscription [${s.name}]. Reason: ${error.message || error}`\n    } finally {\n      s.updating = false\n    }\n\n    eventBus.emit('subscriptionChange', { id })\n\n    return `Subscription [${s.name}] updated successfully.`\n  }\n\n  const updateSubscribes = async () => {\n    let needSave = false\n\n    const update = async (s: Subscription) => {\n      const result = { ok: true, id: s.id, name: s.name, result: '' }\n      try {\n        s.updating = true\n        await _doUpdateSub(s)\n        needSave = true\n        result.result = `Subscription [${s.name}] updated successfully.`\n      } catch (error: any) {\n        result.ok = false\n        result.result = `Failed to update subscription [${s.name}]. Reason: ${error.message || error}`\n      } finally {\n        s.updating = false\n      }\n      return result\n    }\n\n    const result = await asyncPool(\n      5,\n      subscribes.value.filter((v) => !v.disabled),\n      update,\n    )\n\n    if (needSave) await saveSubscribes()\n\n    eventBus.emit('subscriptionsChange', undefined)\n\n    return result.flatMap((v) => (v.ok && v.value) || [])\n  }\n\n  const getSubscribeById = (id: string) => subscribes.value.find((v) => v.id === id)\n\n  const getSubscribeTemplate = (name = '', options: { url?: string } = {}): Subscription => {\n    const id = sampleID()\n    return {\n      id: id,\n      name: name,\n      upload: 0,\n      download: 0,\n      total: 0,\n      expire: 0,\n      updateTime: 0,\n      type: 'Http',\n      url: options.url || '',\n      website: '',\n      path: `data/subscribes/${id}.json`,\n      include: '',\n      exclude: '',\n      includeProtocol: '',\n      excludeProtocol: DefaultExcludeProtocols,\n      proxyPrefix: '',\n      disabled: false,\n      inSecure: false,\n      requestMethod: RequestMethod.Get,\n      requestTimeout: 15,\n      header: {\n        request: {},\n        response: {},\n      },\n      proxies: [],\n      script: DefaultSubscribeScript,\n    }\n  }\n\n  return {\n    subscribes,\n    setupSubscribes,\n    saveSubscribes,\n    addSubscribe,\n    editSubscribe,\n    deleteSubscribe,\n    updateSubscribe,\n    updateSubscribes,\n    getSubscribeById,\n    importSubscribe,\n    getSubscribeTemplate,\n  }\n})\n"
  },
  {
    "path": "frontend/src/types/app.d.ts",
    "content": "import { h, ref, type VNode } from 'vue'\n\nimport type {\n  Lang,\n  Theme,\n  Color,\n  View,\n  WindowStartState,\n  WebviewGpuPolicy,\n  Branch,\n  ControllerCloseMode,\n  PluginTrigger,\n  ScheduledTasksType,\n  RequestMethod,\n} from '@/enums/app'\n\nexport interface TrayContent {\n  icon?: string\n  title?: string\n  tooltip?: string\n}\n\nexport interface Menu {\n  label: string\n  handler?: (...args: any) => void\n  separator?: boolean\n  children?: Menu[]\n}\n\nexport interface MenuItem {\n  type: 'item' | 'separator'\n  text?: string\n  tooltip?: string\n  event?: (() => void) | string\n  children?: MenuItem[]\n  hidden?: boolean\n  checked?: boolean\n}\n\nexport interface AppSettings {\n  lang: Lang | string\n  theme: Theme\n  color: Color\n  primaryColor: string\n  secondaryColor: string\n  fontFamily: string\n  profilesView: View\n  subscribesView: View\n  rulesetsView: View\n  pluginsView: View\n  scheduledtasksView: View\n  windowStartState: WindowStartState\n  webviewGpuPolicy: WebviewGpuPolicy\n  width: number\n  height: number\n  exitOnClose: boolean\n  closeKernelOnExit: boolean\n  autoSetSystemProxy: boolean\n  proxyBypassList: string\n  autoStartKernel: boolean\n  autoRestartKernel: boolean\n  userAgent: string\n  startupDelay: number\n  connections: {\n    visibility: Record<string, boolean>\n    order: string[]\n  }\n  kernel: {\n    realMemoryUsage: boolean\n    branch: Branch\n    profile: string\n    autoClose: boolean\n    unAvailable: boolean\n    cardMode: boolean\n    cardColumns: number\n    sortByDelay: boolean\n    testUrl: string\n    testTimeout: number\n    concurrencyLimit: number\n    controllerCloseMode: ControllerCloseMode\n    controllerSensitivity: number\n    main: {\n      env: Recordable\n      args: string[]\n    }\n    alpha: {\n      env: Recordable\n      args: string[]\n    }\n  }\n  pluginSettings: Record<string, Record<string, any>>\n  githubApiToken: string\n  multipleInstance: boolean\n  addPluginToMenu: boolean\n  addGroupToMenu: boolean\n  rollingRelease: boolean\n  debugOutline: boolean\n  debugNoAnimation: boolean\n  debugNoRounded: false\n  debugBorder: boolean\n  pages: string[]\n}\n\nexport interface PluginConfiguration {\n  id: string\n  title: string\n  description: string\n  key: string\n  component:\n    | 'CheckBox'\n    | 'CodeViewer'\n    | 'Input'\n    | 'InputList'\n    | 'KeyValueEditor'\n    | 'Radio'\n    | 'Select'\n    | 'MultipleSelect'\n    | 'Switch'\n    | 'ColorPicker'\n    | ''\n  value: any\n  options: any[]\n}\n\nexport interface Plugin {\n  id: string\n  version: string\n  name: string\n  description: string\n  type: 'Http' | 'File'\n  url: string\n  path: string\n  triggers: PluginTrigger[]\n  tags: string[]\n  hasUI: boolean\n  menus: Record<string, string>\n  context: {\n    profiles: Recordable\n    subscriptions: Recordable\n    rulesets: Recordable\n    plugins: Recordable\n    scheduledtasks: Recordable\n  }\n  configuration: PluginConfiguration[]\n  disabled: boolean\n  install: boolean\n  installed: boolean\n  status: number // 0: Normal 1: Running 2: Stopped\n  // Not Config\n  updating?: boolean\n  loading?: boolean\n  running?: boolean\n}\n\nexport interface ScheduledTask {\n  id: string\n  name: string\n  type: ScheduledTasksType\n  subscriptions: string[]\n  rulesets: string[]\n  plugins: string[]\n  script: string\n  cron: string\n  notification: boolean\n  disabled: boolean\n  lastTime: number\n}\n\nexport interface Subscription {\n  id: string\n  name: string\n  upload: number\n  download: number\n  total: number\n  expire: number\n  updateTime: number\n  type: 'Http' | 'File' | 'Manual'\n  url: string\n  website: string\n  path: string\n  include: string\n  exclude: string\n  includeProtocol: string\n  excludeProtocol: string\n  proxyPrefix: string\n  disabled: boolean\n  inSecure: boolean\n  proxies: { id: string; tag: string; type: string }[]\n  requestMethod: RequestMethod\n  requestTimeout: number\n  header: {\n    request: Recordable\n    response: Recordable\n  }\n  script: string\n  // Not Config\n  updating?: boolean\n}\n\n// Custom Action\nexport interface CustomActionApi {\n  h: typeof h\n  ref: typeof ref\n}\ntype CustomActionProps = Recordable\ntype CustomActionSlots = Recordable<\n  ((api: CustomActionApi) => VNode | string | number | boolean) | VNode | string | number | boolean\n>\nexport interface CustomAction<P = CustomActionProps, S = CustomActionSlots> {\n  id?: string\n  component: string\n  componentProps?: P | ((api: CustomActionApi) => P)\n  componentSlots?: S | ((api: CustomActionApi) => S)\n}\nexport type CustomActionFn = ((api: CustomActionApi) => CustomAction) & {\n  id?: string\n}\n"
  },
  {
    "path": "frontend/src/types/global.d.ts",
    "content": "interface Window {\n  /**\n   * The variable is initialized in `globalMethods.ts:11`\n   */\n  Plugins: any\n  /**\n   * The variable is initialized in `globalMethods.ts:23`\n   */\n  AsyncFunction: FunctionConstructor\n  /**\n   * The variable is initialized in `globalMethods.ts:21`\n   */\n  Vue: any\n  /**\n   * The variable is initialized in `main.ts:15`\n   */\n  appInstance: any\n}\n"
  },
  {
    "path": "frontend/src/types/kernel.d.ts",
    "content": "export interface CoreApiConfig {\n  port: number\n  'socks-port': number\n  'mixed-port': number\n  'interface-name': string\n  'allow-lan': boolean\n  mode: string\n  tun: {\n    enable: boolean\n    stack: string\n    device: string\n  }\n}\n\nexport interface CoreApiProxy {\n  alive: boolean\n  all: string[]\n  name: string\n  now: string\n  type: string\n  udp: boolean\n  history: {\n    delay: number\n  }[]\n}\n\nexport interface CoreApiProxies {\n  proxies: Record<string, Proxy>\n}\n\nexport interface CoreApiConnections {\n  connections: {\n    id: string\n    chains: string[]\n  }[]\n}\n\nexport interface CoreApiTrafficData {\n  down: number\n  up: number\n}\n\nexport interface CoreApiMemoryData {\n  inuse: number\n  oslimit: number\n}\n\nexport interface CoreApiLogsData {\n  type: string\n  payload: string\n}\n\nexport interface CoreApiConnectionsData {\n  memory: number\n  uploadTotal: number\n  downloadTotal: number\n  connections: {\n    chains: string[]\n    download: number\n    id: string\n    metadata: {\n      destinationIP: string\n      destinationPort: string\n      dnsMode: string\n      host: string\n      network: string\n      processPath: string\n      sourceIP: string\n      sourcePort: string\n      type: string\n    }\n    rule: string\n    rulePayload: string\n    start: string\n    upload: number\n  }[]\n}\n\nexport type CoreApiWsDataMap = {\n  logs: CoreApiLogsData\n  memory: CoreApiMemoryData\n  traffic: CoreApiTrafficData\n  connections: CoreApiConnectionsData\n}\n"
  },
  {
    "path": "frontend/src/types/profile.d.ts",
    "content": "type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'panic'\ninterface ILog {\n  disabled: boolean\n  level: LogLevel\n  output: string\n  timestamp: boolean\n}\n\ninterface IExperimental {\n  clash_api: {\n    external_controller: string\n    external_ui: string\n    external_ui_download_url: string\n    external_ui_download_detour: string\n    secret: string\n    default_mode: string\n    access_control_allow_origin: string[]\n    access_control_allow_private_network: boolean\n  }\n  cache_file: {\n    enabled: boolean\n    path: string\n    cache_id: string\n    store_fakeip: boolean\n    store_rdrc: boolean\n    rdrc_timeout: string\n  }\n}\n\ninterface IProxy {\n  id: string\n  type: string\n  tag: string\n}\n\ntype RuleSetType = 'inline' | 'local' | 'remote'\ntype RuleSetFormat = 'source' | 'binary'\ninterface IRuleSet {\n  id: string\n  type: RuleSetType\n  tag: string\n  // inline\n  rules: string\n  // local\n  path: string\n  // remote\n  url: string\n  download_detour: string\n  update_interval: string\n  // local or remote\n  format: RuleSetFormat\n}\n\ntype InboundType = 'mixed' | 'socks' | 'http' | 'tun'\ntype InboundListen = {\n  listen: string\n  listen_port: number\n  tcp_fast_open: boolean\n  tcp_multi_path: boolean\n  udp_fragment: boolean\n}\n\ninterface IInbound {\n  id: string\n  type: InboundType\n  tag: string\n  enable: boolean\n  mixed?: {\n    listen: InboundListen\n    users: string[]\n  }\n  socks?: {\n    listen: InboundListen\n    users: string[]\n  }\n  http?: {\n    listen: InboundListen\n    users: string[]\n  }\n  tun?: {\n    interface_name: string\n    address: string[]\n    mtu: number\n    auto_route: boolean\n    strict_route: boolean\n    route_address: string[]\n    route_exclude_address: string[]\n    endpoint_independent_nat: boolean\n    stack: TunStackEnum\n  }\n}\n\ntype OutboundType = 'direct' | 'block' | 'selector' | 'urltest'\n\ntype RuleAction = 'route' | 'route-options' | 'reject' | 'hijack-dns' | 'sniff' | 'resolve'\ntype DnsRuleAction = 'route' | 'route-options' | 'reject' | 'predefined'\n\ninterface IOutbound {\n  id: string\n  tag: string\n  type: OutboundType\n  outbounds: IProxy[]\n  url: string\n  interval: string\n  tolerance: number\n  interrupt_exist_connections: boolean\n  // gui\n  include: string\n  exclude: string\n}\n\ntype RuleType =\n  | 'inbound'\n  | 'network'\n  | 'protocol'\n  | 'domain'\n  | 'domain_suffix'\n  | 'domain_keyword'\n  | 'domain_regex'\n  | 'source_ip_cidr'\n  | 'ip_cidr'\n  | 'source_port'\n  | 'source_port_range'\n  | 'port'\n  | 'port_range'\n  | 'process_name'\n  | 'process_path'\n  | 'process_path_regex'\n  | 'rule_set'\n  | 'ip_is_private'\n  | 'clash_mode'\n  | 'outbound'\n  | 'inline'\n  | 'InsertionPoint'\n\ninterface IRule {\n  id: string\n  type: RuleType\n  enable: boolean\n  payload: string\n  invert: boolean\n  action: RuleAction\n  // action = route\n  outbound: string\n  // action = sniff\n  sniffer: string[]\n  // action = resolve\n  strategy: Strategy\n  server: string\n}\n\ninterface IRoute {\n  rules: IRule[]\n  rule_set: IRuleSet[]\n  final: string\n  auto_detect_interface: boolean\n  default_interface: string\n  find_process: boolean\n  default_domain_resolver: {\n    server: string\n    client_subnet: string\n  }\n}\n\ntype Strategy = 'default' | 'prefer_ipv4' | 'prefer_ipv6' | 'ipv4_only' | 'ipv6_only'\ntype DNSServer =\n  | 'local'\n  | 'hosts'\n  | 'tcp'\n  | 'udp'\n  | 'tls'\n  | 'quic'\n  | 'https'\n  | 'h3'\n  | 'dhcp'\n  | 'fakeip'\n  | 'tailscale'\n\ninterface IDNSServer {\n  id: string\n  tag: string\n  type: DNSServer\n  // [local,tcp,udp,tls,quic,https/h3,dhcp]\n  detour: string\n  domain_resolver: string\n  // hosts\n  hosts_path: string[]\n  predefined: Recordable\n  // [tcp,udp,tls,quic/https,h3]\n  server: string\n  server_port: string\n  // [https,h3]\n  path: string\n  // dhcp\n  interface: string\n  // fakeip\n  inet4_range: string\n  inet6_range: string\n}\n\ninterface IDNSRule {\n  id: string\n  type: RuleType\n  enable: boolean\n  payload: string\n  action: DnsRuleAction\n  invert: boolean\n  // route\n  server: string\n  strategy: Strategy\n  // route/route-options\n  disable_cache: boolean\n  client_subnet: string\n}\n\ninterface IDNS {\n  servers: IDNSServer[]\n  rules: IDNSRule[]\n  disable_cache: boolean\n  disable_expire: boolean\n  independent_cache: boolean\n  client_subnet: string\n  final: string\n  strategy: Strategy\n}\n\ntype MixinPriority = 'mixin' | 'gui'\n\ninterface IMixin {\n  priority: MixinPriority\n  format: 'json' | 'yaml'\n  config: string\n}\n\ninterface IScript {\n  code: string\n}\n\ninterface IProfile {\n  id: string\n  name: string\n  log: ILog\n  experimental: IExperimental\n  inbounds: IInbound[]\n  outbounds: IOutbound[]\n  route: IRoute\n  dns: IDNS\n  mixin: IMixin\n  script: IScript\n}\n"
  },
  {
    "path": "frontend/src/types/typescript.d.ts",
    "content": "type Recordable<T = any> = { [x: string]: T }\n\ntype MaybePromise<T> = T | Promise<T>\n"
  },
  {
    "path": "frontend/src/utils/command.ts",
    "content": "import { RestartApp } from '@/bridge'\nimport { ColorOptions, ThemeOptions } from '@/constant/app'\nimport { ModeOptions } from '@/constant/kernel'\nimport { PluginTrigger, PluginTriggerEvent } from '@/enums/app'\nimport useI18n from '@/lang'\nimport {\n  useAppSettingsStore,\n  useAppStore,\n  useEnvStore,\n  useKernelApiStore,\n  usePluginsStore,\n  useRulesetsStore,\n  useSubscribesStore,\n} from '@/stores'\nimport { exitApp, handleChangeMode, message, reloadApp } from '@/utils'\n\ntype Command = {\n  label: string\n  cmd: string\n  desc?: string\n  handler?: () => Promise<any> | any\n  children?: Command[]\n}\n\nconst processCommands = (commands: Command[], parentLabel = '', parentCmd = '') => {\n  const { t } = useI18n.global\n\n  const result: Command[] = []\n\n  commands.forEach((item) => {\n    const label = parentLabel ? `${t(parentLabel)}: ${t(item.label)}` : t(item.label)\n    const cmd = parentCmd ? `${parentCmd}: ${item.cmd}` : item.cmd\n\n    if (item.children) {\n      result.push(...processCommands(item.children, label, cmd))\n    } else {\n      result.push({ label, cmd, handler: item.handler })\n    }\n  })\n\n  return result\n}\n\nexport const getCommands = () => {\n  const kernelStore = useKernelApiStore()\n  const appSettings = useAppSettingsStore()\n  const envStore = useEnvStore()\n  const appStore = useAppStore()\n  const subscriptionsStore = useSubscribesStore()\n  const rulesetsStore = useRulesetsStore()\n  const pluginsStore = usePluginsStore()\n\n  const rawCommands: Command[] = [\n    {\n      label: 'tray.kernel',\n      cmd: 'Core',\n      children: [\n        {\n          label: 'tray.startKernel',\n          cmd: 'Start Core',\n          handler: kernelStore.startCore,\n        },\n        {\n          label: 'tray.stopKernel',\n          cmd: 'Stop Core',\n          handler: kernelStore.stopCore,\n        },\n        {\n          label: 'tray.restartKernel',\n          cmd: 'Restart Core',\n          handler: kernelStore.restartCore,\n        },\n        {\n          label: 'tray.enableTunMode',\n          cmd: 'Enable Tun',\n          handler: () => kernelStore.updateConfig('tun', { enable: true }),\n        },\n        {\n          label: 'tray.disableTunMode',\n          cmd: 'Disable Tun',\n          handler: () => kernelStore.updateConfig('tun', { enable: false }),\n        },\n        {\n          label: 'kernel.allow-lan',\n          cmd: 'Allow Lan',\n          handler: () => kernelStore.updateConfig('allow-lan', true),\n        },\n        {\n          label: 'kernel.disallow-lan',\n          cmd: 'Disallow Lan',\n          handler: () => kernelStore.updateConfig('allow-lan', false),\n        },\n        {\n          label: 'kernel.mode',\n          cmd: 'Core Mode',\n          children: ModeOptions.map((mode) => ({\n            label: mode.label,\n            cmd: mode.value,\n            handler: () => handleChangeMode(mode.value),\n          })),\n        },\n      ],\n    },\n    {\n      label: 'tray.proxy',\n      cmd: 'System Proxy',\n      children: [\n        {\n          label: 'tray.setSystemProxy',\n          cmd: 'Set System Proxy',\n          handler: envStore.setSystemProxy,\n        },\n        {\n          label: 'tray.clearSystemProxy',\n          cmd: 'Clear System Proxy',\n          handler: envStore.clearSystemProxy,\n        },\n      ],\n    },\n    {\n      label: 'APP',\n      cmd: 'APP',\n      children: [\n        {\n          label: 'settings.lang.name',\n          cmd: 'Language',\n          children: [\n            {\n              label: 'settings.lang.load',\n              cmd: 'Load language files',\n              handler: async () => {\n                await appStore.loadLocales()\n                message.success('common.success')\n              },\n            },\n            ...appStore.locales.map((v) => ({\n              label: v.label,\n              cmd: v.value,\n              handler: () => (appSettings.app.lang = v.value),\n            })),\n          ],\n        },\n        {\n          label: 'settings.theme.name',\n          cmd: 'Theme',\n          children: ThemeOptions.map((theme) => ({\n            label: theme.label,\n            cmd: theme.value,\n            handler: () => (appSettings.app.theme = theme.value),\n          })),\n        },\n        {\n          label: 'settings.color.name',\n          cmd: 'Color',\n          children: ColorOptions.map((color) => ({\n            label: color.label,\n            cmd: color.value,\n            handler: () => (appSettings.app.color = color.value),\n          })),\n        },\n        {\n          label: 'titlebar.reload',\n          cmd: 'Reload Window',\n          handler: reloadApp,\n        },\n        {\n          label: 'tray.restartTip',\n          cmd: 'Restart APP',\n          handler: RestartApp,\n        },\n        {\n          label: 'tray.exitTip',\n          cmd: 'Exit APP',\n          handler: exitApp,\n        },\n        {\n          label: 'router.about',\n          cmd: 'About APP',\n          handler: () => (appStore.showAbout = true),\n        },\n      ],\n    },\n    {\n      label: 'router.subscriptions',\n      cmd: 'Subscriptions',\n      children: [\n        {\n          label: 'common.updateAll',\n          cmd: 'Update Subscriptions',\n          handler: subscriptionsStore.updateSubscribes,\n        },\n      ],\n    },\n    {\n      label: 'router.rulesets',\n      cmd: 'Rulesets',\n      children: [\n        {\n          label: 'common.updateAll',\n          cmd: 'Update Rulesets',\n          handler: rulesetsStore.updateRulesets,\n        },\n      ],\n    },\n    {\n      label: 'router.plugins',\n      cmd: 'Plugins',\n      children: [\n        {\n          label: 'common.updateAll',\n          cmd: 'Update Plugins',\n          handler: pluginsStore.updatePlugins,\n        },\n      ],\n    },\n    {\n      label: 'tray.plugins',\n      cmd: 'Plugins',\n      children: pluginsStore.plugins.flatMap((plugin) => {\n        const hasTrigger = !!plugin.triggers.find((trigger) => trigger === PluginTrigger.OnManual)\n        const hasMenus = !!Object.keys(plugin.menus).length\n        if (!hasTrigger && !hasMenus) return []\n        const children: Command[] = []\n        if (hasTrigger) {\n          children.push({\n            label: 'common.run',\n            cmd: PluginTrigger.OnManual,\n            handler: async () => {\n              plugin.running = true\n              try {\n                await pluginsStore.manualTrigger(plugin.id, PluginTriggerEvent.OnManual)\n              } catch (error: any) {\n                message.error(error)\n              }\n              plugin.running = false\n            },\n          })\n        }\n        if (hasMenus) {\n          Object.entries(plugin.menus).forEach(([title, fnName]) => {\n            children.push({\n              label: title,\n              cmd: fnName,\n              handler: async () => {\n                try {\n                  plugin.running = true\n                  await pluginsStore.manualTrigger(plugin.id, fnName as any)\n                } catch (error: any) {\n                  message.error(error.message || error)\n                } finally {\n                  plugin.running = false\n                }\n              },\n            })\n          })\n        }\n        return { label: plugin.name, cmd: plugin.id, children }\n      }),\n    },\n  ]\n\n  return processCommands(rawCommands)\n}\n"
  },
  {
    "path": "frontend/src/utils/completion.ts",
    "content": "import { snippetCompletion, completeFromList } from '@codemirror/autocomplete'\nimport { scopeCompletionSource, localCompletionSource, snippets } from '@codemirror/lang-javascript'\n\nimport { PluginTriggerEvent } from '@/enums/app'\nimport i18n from '@/lang'\n\nimport type { CompletionContext, Completion } from '@codemirror/autocomplete'\n\nexport const getCompletions = (pluginScope: any = undefined) => {\n  const { t } = i18n.global\n\n  const snippetsCompletions: Completion[] = [\n    /**\n     * Built-In\n     */\n    ...snippets,\n    /**\n     * Plugin Triggers\n     */\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('common.install')} */\\n` +\n        `const ${PluginTriggerEvent.OnInstall} = async () => {\\n\\t\\${}\\n\\treturn 0\\n}`,\n      {\n        label: PluginTriggerEvent.OnInstall,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('common.install'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('common.uninstall')} */\\n` +\n        `const ${PluginTriggerEvent.OnUninstall} = async () => {\\n\\t\\${}\\n\\treturn 0\\n}`,\n      {\n        label: PluginTriggerEvent.OnUninstall,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('common.uninstall'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::manual')} */\\n` +\n        `const ${PluginTriggerEvent.OnManual} = async () => {\\n\\t\\${}\\n}`,\n      {\n        label: PluginTriggerEvent.OnManual,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::manual'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::tray::update')} */\\n` +\n        `const ${PluginTriggerEvent.OnTrayUpdate} = async (tray, menus) => {\\n\\t\\${}\\n\\treturn { tray, menus }\\n}`,\n      {\n        label: PluginTriggerEvent.OnTrayUpdate,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::tray::update'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::subscribe')} */\\n` +\n        `const ${PluginTriggerEvent.OnSubscribe} = async (proxies, subscription) => {\\n\\t\\${}\\n\\treturn proxies\\n}`,\n      {\n        label: PluginTriggerEvent.OnSubscribe,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::subscribe'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::generate')} */\\n` +\n        `const ${PluginTriggerEvent.OnGenerate} = async (config, profile) => {\\n\\t\\${}\\n\\treturn config\\n}`,\n      {\n        label: PluginTriggerEvent.OnGenerate,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::generate'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::startup')} */\\n` +\n        `const ${PluginTriggerEvent.OnStartup} = async () => {\\n\\t\\${}\\n}`,\n      {\n        label: PluginTriggerEvent.OnStartup,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::startup'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::shutdown')} */\\n` +\n        `const ${PluginTriggerEvent.OnShutdown} = async () => {\\n\\t\\${}\\n}`,\n      {\n        label: PluginTriggerEvent.OnShutdown,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::shutdown'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::core::started')} */\\n` +\n        `const ${PluginTriggerEvent.OnCoreStarted} = async () => {\\n\\t\\${}\\n}`,\n      {\n        label: PluginTriggerEvent.OnCoreStarted,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::core::started'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::core::stopped')} */\\n` +\n        `const ${PluginTriggerEvent.OnCoreStopped} = async () => {\\n\\t\\${}\\n}`,\n      {\n        label: PluginTriggerEvent.OnCoreStopped,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::core::stopped'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::before::core::start')} */\\n` +\n        `const ${PluginTriggerEvent.OnBeforeCoreStart} = async (config, profile) => {\\n\\t\\${}\\n\\treturn config\\n}`,\n      {\n        label: PluginTriggerEvent.OnBeforeCoreStart,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::before::core::start'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::before::core::stop')} */\\n` +\n        `const ${PluginTriggerEvent.OnBeforeCoreStop} = async () => {\\n\\t\\${}\\n}`,\n      {\n        label: PluginTriggerEvent.OnBeforeCoreStop,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::before::core::stop'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::ready')} */\\n` +\n        `const ${PluginTriggerEvent.OnReady} = async () => {\\n\\t\\${}\\n}`,\n      {\n        label: PluginTriggerEvent.OnReady,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::ready'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::reload')} */\\n` +\n        `const ${PluginTriggerEvent.OnReload} = async () => {\\n\\t\\${}\\n}`,\n      {\n        label: PluginTriggerEvent.OnReload,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::reload'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::task')} */\\n` +\n        `const ${PluginTriggerEvent.OnTask} = async () => {\\n\\t\\${}\\n}`,\n      {\n        label: PluginTriggerEvent.OnTask,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::task'),\n      },\n    ),\n    snippetCompletion(\n      `/* ${t('plugin.trigger') + ' ' + t('plugin.on::configure')} */\\n` +\n        `const ${PluginTriggerEvent.OnConfigure} = async (config, old) => {\\n\\t\\${}\\n}`,\n      {\n        label: PluginTriggerEvent.OnConfigure,\n        type: 'keyword',\n        detail: t('plugin.trigger') + ' ' + t('plugin.on::configure'),\n      },\n    ),\n    /**\n     * Others\n     */\n    snippetCompletion('console.log(`[$\\\\{Plugin.name\\\\}]`, ${})', {\n      label: 'log',\n      type: 'keyword',\n    }),\n    snippetCompletion(\n      \"const { close } = await Plugins.StartServer('${address}', '${serverID}', async (req, res) => {\\n\\tres.end(200, {'Content-Type': 'application/json'}, 'Server is running...')\\n})\",\n      {\n        label: 'StartServer',\n        type: 'keyword',\n      },\n    ),\n    snippetCompletion(\n      \"await Plugins.Download('${url}', '${path}', {${headers}}, (progress, total) => {\\n\\t${}\\n})\",\n      {\n        label: 'Download',\n        type: 'keyword',\n      },\n    ),\n    snippetCompletion(\n      \"await Plugins.Upload('${url}', '${path}', {${headers}}, (progress, total) => {\\n\\t${}\\n})\",\n      {\n        label: 'Upload',\n        type: 'keyword',\n      },\n    ),\n    snippetCompletion(\n      \"const { status, headers, body } = await Plugins.Requests({\\n\\turl: '${url}', \\n\\tmethod: '${GET}', \\n\\theaders: {}, \\n\\tbody: '${body}'\\n})\",\n      {\n        label: 'Requests',\n        type: 'keyword',\n      },\n    ),\n    snippetCompletion(\n      \"const pid = await Plugins.ExecBackground(\\n\\t'${path}', \\n\\t[${args}], \\n\\tasync (out) => {\\n\\t\\t${}\\n\\t}, \\n\\tasync () => {\\n\\t\\t${}\\n\\t}\\n)\",\n      {\n        label: 'ExecBackground',\n        type: 'keyword',\n      },\n    ),\n  ]\n\n  const completions = [\n    /**\n     * Global methods include all APIs of `Plugins` and `Plugin Metadata`\n     */\n    scopeCompletionSource({ ...window, Plugin: pluginScope }),\n    /**\n     * Code Snippets\n     */\n    completeFromList(snippetsCompletions),\n    /**\n     * Locally Defined\n     */\n    (context: CompletionContext) => {\n      const word = context.matchBefore(/\\w*/)\n      if (!word || context.explicit) return null\n\n      const codeCompletion = localCompletionSource(context) || { options: [] }\n\n      return {\n        from: word.from,\n        options: codeCompletion.options,\n      }\n    },\n  ]\n\n  return completions\n}\n"
  },
  {
    "path": "frontend/src/utils/env.ts",
    "content": "export const APP_TITLE = import.meta.env.VITE_APP_TITLE\n\nexport const APP_VERSION = import.meta.env.VITE_APP_VERSION\n\nexport const APP_VERSION_API = import.meta.env.VITE_APP_VERSION_API\n\nexport const APP_LOCALES_URL = import.meta.env.VITE_APP_LOCALES_URL\n\nexport const PROJECT_URL = import.meta.env.VITE_APP_PROJECT_URL\n\nexport const TG_GROUP = import.meta.env.VITE_APP_TG_GROUP\n\nexport const TG_CHANNEL = import.meta.env.VITE_APP_TG_CHANNEL\n\nexport const isDev = import.meta.env.DEV\n"
  },
  {
    "path": "frontend/src/utils/eventBus.ts",
    "content": "type EventMap = {\n  profileChange: { id: string }\n  subscriptionChange: { id: string }\n  subscriptionsChange: void\n  rulesetChange: { id: string }\n  rulesetsChange: void\n}\n\nclass TypedEventBus<Events extends Record<string, any>> {\n  private handlers: {\n    [K in keyof Events]?: ((data: Events[K]) => void)[]\n  } = {}\n\n  on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void) {\n    const list = this.handlers[event] || []\n    list.push(handler)\n    this.handlers[event] = list\n  }\n\n  off<K extends keyof Events>(event: K, handler: (data: Events[K]) => void) {\n    const list = this.handlers[event]\n    if (!list) return\n    this.handlers[event] = list.filter((h) => h !== handler)\n  }\n\n  emit<K extends keyof Events>(event: K, data: Events[K]) {\n    const list = this.handlers[event]\n    if (!list) return\n    list.forEach((h) => h(data))\n  }\n}\n\nexport const eventBus = new TypedEventBus<EventMap>()\n"
  },
  {
    "path": "frontend/src/utils/format.ts",
    "content": "import i18n from '@/lang'\n\nexport function formatBytes(bytes: number, decimals: number = 1): string {\n  if (bytes === 0) return '0 B'\n\n  const k = 1024\n  const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n\n  const i = Math.max(0, Math.floor(Math.log(bytes) / Math.log(k)))\n  const formattedValue = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))\n\n  return `${formattedValue} ${sizes[i]}`\n}\n\nexport function formatRelativeTime(d: string | number) {\n  const formatter = new Intl.RelativeTimeFormat(i18n.global.locale.value, { numeric: 'auto' })\n  const date = new Date(d)\n  const now = Date.now()\n  const diffMs = date.getTime() - now\n\n  const isSameDay = formatDate(d, 'YYYY-MM-DD') === formatDate(now, 'YYYY-MM-DD')\n\n  // now\n  if (diffMs === 0) return formatter.format(0, 'second')\n\n  const units: { unit: Intl.RelativeTimeFormatUnit; threshold: number }[] = [\n    { unit: 'year', threshold: 365 * 24 * 60 * 60 * 1000 },\n    { unit: 'month', threshold: 30 * 24 * 60 * 60 * 1000 },\n    { unit: 'day', threshold: 24 * 60 * 60 * 1000 },\n    { unit: 'hour', threshold: 60 * 60 * 1000 },\n    { unit: 'minute', threshold: 60 * 1000 },\n    { unit: 'second', threshold: 1000 },\n  ]\n\n  for (const { unit, threshold } of units) {\n    if (unit === 'day' && isSameDay) continue\n    const amount = Math.round(diffMs / threshold)\n    if (Math.abs(amount) > 0) return formatter.format(amount, unit)\n  }\n\n  return formatter.format(Math.round(diffMs / 1000), 'second')\n}\n\nexport function formatDate(timestamp: number | string, format: string) {\n  const date = new Date(timestamp)\n\n  const map: Record<string, any> = {\n    YYYY: date.getFullYear(),\n    MM: String(date.getMonth() + 1).padStart(2, '0'),\n    DD: String(date.getDate()).padStart(2, '0'),\n    HH: String(date.getHours()).padStart(2, '0'),\n    mm: String(date.getMinutes()).padStart(2, '0'),\n    ss: String(date.getSeconds()).padStart(2, '0'),\n  }\n\n  return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (matched) => map[matched])\n}\n"
  },
  {
    "path": "frontend/src/utils/generator.ts",
    "content": "import { parse } from 'yaml'\n\nimport { ReadFile, WriteFile } from '@/bridge'\nimport { CoreConfigFilePath } from '@/constant/kernel'\nimport {\n  DnsServer,\n  Inbound,\n  LogLevel,\n  Outbound,\n  RuleAction,\n  RulesetType,\n  RuleType,\n  Strategy,\n} from '@/enums/kernel'\nimport { Branch } from '@/enums/app'\nimport {\n  useAppSettingsStore,\n  usePluginsStore,\n  useRulesetsStore,\n  useSubscribesStore,\n} from '@/stores'\nimport { deepAssign, deepClone, APP_TITLE, buildSmartRegExp } from '@/utils'\n\nconst _generateRule = (rule: IRule | IDNSRule, rule_set: IRuleSet[], inbounds: IInbound[]) => {\n  const getInbound = (id: string) => inbounds.find((v) => v.id === id)?.tag\n  const getRuleset = (id: string) => rule_set.find((v) => v.id === id)?.tag\n\n  const extra: Recordable = { action: rule.action, invert: rule.invert ? true : undefined }\n  if (rule.type === RuleType.Inline) {\n    deepAssign(extra, JSON.parse(rule.payload))\n  } else if (rule.type === RuleType.RuleSet) {\n    extra[rule.type] = rule.payload.split(',').map((id) => getRuleset(id))\n  } else if (rule.type === RuleType.Inbound) {\n    extra[rule.type] = getInbound(rule.payload)\n  } else if ([RuleType.IpIsPrivate, RuleType.IpAcceptAny].includes(rule.type as any)) {\n    extra[rule.type] = rule.payload === 'true'\n  } else if (rule.type === RuleType.ClashMode) {\n    extra[rule.type] = rule.payload\n  } else {\n    extra[rule.type] = String(rule.payload)\n      .split(',')\n      .map((val) => {\n        if ([RuleType.Port, RuleType.SourcePort].includes(rule.type as any)) {\n          return Number(val)\n        }\n        return val\n      })\n    if (extra[rule.type].length === 1) {\n      extra[rule.type] = extra[rule.type][0]\n    }\n  }\n  return extra\n}\n\nconst generateExperimental = (experimental: IExperimental, outbounds: IOutbound[]) => {\n  const getOutbound = (id: string) => outbounds.find((v) => v.id === id)?.tag\n  return {\n    clash_api: {\n      ...experimental.clash_api,\n      external_ui_download_detour: getOutbound(experimental.clash_api.external_ui_download_detour),\n    },\n    cache_file: experimental.cache_file,\n  }\n}\n\nconst generateInbounds = (inbounds: IInbound[]) => {\n  return inbounds.flatMap((inbound) => {\n    if (!inbound.enable) return []\n    if (inbound.type !== Inbound.Tun) {\n      const users = inbound[inbound.type]!.users.map((user) => ({\n        username: user.split(':')[0],\n        password: user.split(':')[1],\n      }))\n      return {\n        type: inbound.type,\n        tag: inbound.tag,\n        ...inbound[inbound.type]!.listen,\n        users: users.length > 0 ? users : undefined,\n      }\n    }\n    if (inbound.type === Inbound.Tun) {\n      return {\n        type: inbound.type,\n        tag: inbound.tag,\n        ...inbound.tun!,\n        route_address: inbound.tun!.route_address?.length ? inbound.tun!.route_address : undefined,\n        route_exclude_address: inbound.tun!.route_exclude_address?.length\n          ? inbound.tun!.route_exclude_address\n          : undefined,\n      }\n    }\n  })\n}\n\nconst generateOutbounds = async (outbounds: IOutbound[]) => {\n  const result: Recordable[] = []\n  const SubscriptionCache: Recordable<any[]> = {}\n  const proxiesSet = new Set<any>()\n  const builtInProxiesSet = new Set<string>()\n\n  const createTagMatcher = (include: string, exclude: string) => {\n    const includeRegex = include ? buildSmartRegExp(include) : null\n    const excludeRegex = exclude ? buildSmartRegExp(exclude) : null\n    return (tag: string) => {\n      const flag1 = includeRegex ? includeRegex.test(tag) : true\n      const flag2 = excludeRegex ? excludeRegex.test(tag) : false\n      return flag1 && !flag2\n    }\n  }\n\n  const subscribesStore = useSubscribesStore()\n\n  for (const outbound of outbounds) {\n    const _outbound: Recordable = {\n      type: outbound.type,\n      tag: outbound.tag,\n    }\n    if (outbound.type === Outbound.Urltest) {\n      _outbound.url = outbound.url\n      _outbound.interval = outbound.interval\n      _outbound.tolerance = outbound.tolerance\n    }\n    if (outbound.type === Outbound.Selector || outbound.type === Outbound.Urltest) {\n      _outbound.interrupt_exist_connections = outbound.interrupt_exist_connections\n      _outbound.outbounds = []\n      const isTagMatching = createTagMatcher(outbound.include, outbound.exclude)\n      for (const proxy of outbound.outbounds) {\n        if (proxy.type === 'Built-in') {\n          if ([Outbound.Direct, Outbound.Block].includes(proxy.id as Outbound)) {\n            builtInProxiesSet.add(proxy.id)\n          }\n          _outbound.outbounds.push(proxy.tag)\n        } else {\n          const subId = proxy.type === 'Subscription' ? proxy.id : proxy.type\n          if (!SubscriptionCache[subId]) {\n            const sub = subscribesStore.getSubscribeById(subId)\n            if (sub) {\n              const subStr = await ReadFile(sub.path)\n              const proxies = JSON.parse(subStr)\n              SubscriptionCache[subId] = proxies\n            }\n          }\n          if (proxy.type === 'Subscription') {\n            _outbound.outbounds.push(\n              ...SubscriptionCache[subId]!.map((v) => v.tag).filter((tag) => isTagMatching(tag)),\n            )\n            SubscriptionCache[subId]!.forEach((v) => proxiesSet.add(v))\n          } else {\n            const _proxy = SubscriptionCache[subId]!.find((v) => v.tag === proxy.tag)\n            if (_proxy && isTagMatching(_proxy.tag)) {\n              _outbound.outbounds.push(_proxy.tag)\n              proxiesSet.add(_proxy)\n            }\n          }\n        }\n      }\n    }\n    result.push(_outbound)\n  }\n\n  result.push(...proxiesSet)\n  result.push(...Array.from(builtInProxiesSet).map((v) => ({ type: v, tag: v })))\n\n  return result\n}\n\nconst generateRoute = (route: IRoute, inbounds: IInbound[], outbounds: IOutbound[], dns: IDNS) => {\n  const getOutbound = (id: string) => outbounds.find((v) => v.id === id)?.tag\n  const getDnsServer = (id: string) => dns.servers.find((v) => v.id === id)?.tag\n  const isInboundEnabled = (id: string) => inbounds.find((v) => v.id === id)?.enable\n\n  const rulesetsStore = useRulesetsStore()\n\n  const extra: Recordable = {}\n  if (!route.auto_detect_interface) {\n    extra.default_interface = route.default_interface\n  }\n  return {\n    rules: route.rules.flatMap((rule) => {\n      if (rule.type === RuleType.InsertionPoint || !rule.enable) {\n        return []\n      }\n      if (rule.type === RuleType.Inbound && !isInboundEnabled(rule.payload)) {\n        return []\n      }\n      const extra: Recordable = _generateRule(rule, route.rule_set, inbounds)\n\n      if (rule.action === RuleAction.Route) {\n        extra.outbound = getOutbound(rule.outbound)\n      } else if (rule.action === RuleAction.RouteOptions) {\n        deepAssign(extra, JSON.parse(rule.outbound))\n      } else if (rule.action === RuleAction.Reject) {\n        extra.method = rule.outbound\n      } else if (rule.action === RuleAction.Sniff) {\n        if (rule.sniffer.length) {\n          extra.sniffer = rule.sniffer\n        }\n      } else if (rule.action === RuleAction.Resolve) {\n        if (rule.strategy !== Strategy.Default) {\n          extra.strategy = rule.strategy\n        }\n        extra.server = getDnsServer(rule.server)\n      }\n      if (rule.invert) {\n        extra.invert = true\n      }\n      return extra\n    }),\n    rule_set: route.rule_set.map((ruleset) => {\n      const extra: Recordable = {}\n      if (ruleset.type === RuleType.Inline) {\n        extra.rules = JSON.parse(ruleset.rules)\n      } else if (ruleset.type === RulesetType.Local) {\n        const _ruleset = rulesetsStore.getRulesetById(ruleset.path)\n        extra.path = _ruleset?.path.replace('data/', '../')\n        extra.format = ruleset.format\n      } else if (ruleset.type === RulesetType.Remote) {\n        extra.url = ruleset.url\n        extra.format = ruleset.format\n        extra.download_detour = getOutbound(ruleset.download_detour)\n        if (ruleset.update_interval) {\n          extra.update_interval = ruleset.update_interval\n        }\n      }\n      return {\n        tag: ruleset.tag,\n        type: ruleset.type,\n        ...extra,\n      }\n    }),\n    auto_detect_interface: route.auto_detect_interface,\n    find_process: route.find_process ? true : undefined,\n    final: getOutbound(route.final),\n    default_domain_resolver: {\n      server: getDnsServer(route.default_domain_resolver.server),\n    },\n    ...extra,\n  }\n}\n\nconst generateDns = (\n  dns: IDNS,\n  rule_set: IRuleSet[],\n  inbounds: IInbound[],\n  outbounds: IOutbound[],\n) => {\n  const getOutbound = (id: string) => outbounds.find((v) => v.id === id)\n  const getDnsServer = (id: string) => dns.servers.find((v) => v.id === id)?.tag\n  const extra: Recordable = {}\n  if (dns.strategy !== Strategy.Default) {\n    extra.strategy = dns.strategy\n  }\n  if (dns.client_subnet) {\n    extra.client_subnet = dns.client_subnet\n  }\n  return {\n    servers: dns.servers.flatMap((server) => {\n      const extra: Recordable = {}\n      if (\n        [\n          DnsServer.Local,\n          DnsServer.Tcp,\n          DnsServer.Udp,\n          DnsServer.Tls,\n          DnsServer.Quic,\n          DnsServer.Https,\n          DnsServer.H3,\n          DnsServer.Dhcp,\n        ].includes(server.type as any)\n      ) {\n        if (server.detour) {\n          const outbound = getOutbound(server.detour)\n          if (outbound?.type !== Outbound.Direct) {\n            extra.detour = outbound?.tag\n          }\n        }\n        server.domain_resolver && (extra.domain_resolver = getDnsServer(server.domain_resolver))\n        if (\n          [\n            DnsServer.Tcp,\n            DnsServer.Udp,\n            DnsServer.Tls,\n            DnsServer.Quic,\n            DnsServer.Https,\n            DnsServer.H3,\n          ].includes(server.type as any)\n        ) {\n          server.server_port && (extra.server_port = Number(server.server_port))\n          extra.server = server.server\n          if ([DnsServer.Https, DnsServer.H3].includes(server.type as any)) {\n            server.path && (extra.path = server.path)\n          }\n        }\n      }\n      if (server.type === DnsServer.Hosts) {\n        extra.path = server.hosts_path.reduce((p, c) => p.concat(c.split(',')), [] as string[])\n        extra.predefined = Object.entries(server.predefined).reduce(\n          (p, [k, v]) => ({ ...p, [k]: v.split(',') }),\n          {},\n        )\n      } else if (server.type === DnsServer.Dhcp) {\n        server.interface && (extra.interface = server.interface)\n      } else if (server.type === DnsServer.FakeIP) {\n        server.inet4_range && (extra.inet4_range = server.inet4_range)\n        server.inet6_range && (extra.inet6_range = server.inet6_range)\n      }\n      return {\n        tag: server.tag,\n        type: server.type,\n        ...extra,\n      }\n    }),\n    rules: dns.rules.flatMap((rule) => {\n      if (rule.type === RuleType.InsertionPoint || !rule.enable) {\n        return []\n      }\n      const extra: Recordable = _generateRule(rule, rule_set, inbounds)\n      if (rule.type === RuleType.Inline && rule.payload.includes('__is_fake_ip')) {\n        if (!dns.servers.find((v) => v.type === DnsServer.FakeIP)) {\n          return []\n        }\n        delete extra.__is_fake_ip\n      }\n      if ([RuleAction.Route, RuleAction.RouteOptions].includes(rule.action as any)) {\n        rule.disable_cache && (extra.disable_cache = rule.disable_cache)\n        rule.client_subnet && (extra.client_subnet = rule.client_subnet)\n        if (rule.action === RuleAction.Route) {\n          extra.server = getDnsServer(rule.server)\n          if (rule.strategy !== Strategy.Default) {\n            extra.strategy = rule.strategy\n          }\n        }\n      }\n      if ([RuleAction.RouteOptions, RuleAction.Predefined].includes(rule.action as any)) {\n        deepAssign(extra, JSON.parse(rule.server))\n      }\n      if (rule.action === RuleAction.Reject) {\n        extra.method = rule.server\n      }\n      return extra\n    }),\n    disable_cache: dns.disable_cache,\n    disable_expire: dns.disable_expire,\n    independent_cache: dns.independent_cache,\n    final: getDnsServer(dns.final),\n    ...extra,\n  }\n}\n\nexport const generateDnsServerURL = (dnsServer: IDNSServer) => {\n  const { type, server_port, path, server, interface: _interface } = dnsServer\n  let address = ''\n  if (type == DnsServer.Https) {\n    address = `https://${server}${server_port ? ':' + server_port : ''}${path ? path : ''}`\n  } else if (type == DnsServer.H3) {\n    address = `h3://${server}${server_port ? ':' + server_port : ''}${path ? path : ''}`\n  } else if (type == DnsServer.Dhcp) {\n    address = `dhcp://${_interface}`\n  } else if (type == DnsServer.FakeIP) {\n    address =\n      'fake-ip://' +\n      (dnsServer.inet4_range ? dnsServer.inet4_range : '') +\n      (dnsServer.inet6_range ? (dnsServer.inet4_range ? ',' : '') + dnsServer.inet6_range : '')\n  } else if (type === DnsServer.Hosts) {\n    address = 'hosts'\n  } else if (type === DnsServer.Local) {\n    address = 'local'\n  } else {\n    address = `${type}://${server}${server_port ? ':' + server_port : ''}`\n  }\n  return address\n}\n\nconst _adaptToStableBranch = (_: Recordable) => {}\n\ntype GenerateConfigOptions = {\n  enableStableConfigCompat?: boolean\n  enablePluginProcessing?: boolean\n  enableMixinProcessing?: boolean\n  enableScriptProcessing?: boolean\n}\n\nexport const generateConfig = async (\n  originalProfile: IProfile,\n  options: GenerateConfigOptions = {},\n) => {\n  if (typeof options === 'boolean') {\n    options = { enableStableConfigCompat: options }\n  }\n  const appSettings = useAppSettingsStore()\n  const isMainBranch = appSettings.app.kernel.branch === Branch.Main\n\n  const {\n    enableStableConfigCompat = isMainBranch,\n    enablePluginProcessing = true,\n    enableMixinProcessing = true,\n    enableScriptProcessing = true,\n  } = options\n\n  const profile = deepClone(originalProfile)\n  // step 1\n  let config: Recordable = {\n    log: profile.log,\n    experimental: generateExperimental(profile.experimental, profile.outbounds),\n    inbounds: generateInbounds(profile.inbounds),\n    outbounds: await generateOutbounds(profile.outbounds),\n    route: generateRoute(profile.route, profile.inbounds, profile.outbounds, profile.dns),\n    dns: generateDns(profile.dns, profile.route.rule_set, profile.inbounds, profile.outbounds),\n  }\n\n  // adapt to stable branch\n  if (enableStableConfigCompat) {\n    _adaptToStableBranch(config)\n  }\n\n  // step 2\n  if (enablePluginProcessing) {\n    const pluginsStore = usePluginsStore()\n    config = await pluginsStore.onGenerateTrigger(config, originalProfile)\n  }\n\n  // step 3\n  if (enableMixinProcessing) {\n    const { priority, config: mixin } = originalProfile.mixin\n    if (priority === 'mixin') {\n      deepAssign(config, parse(mixin))\n    } else if (priority === 'gui') {\n      deepAssign(config, deepAssign(parse(mixin), config))\n    }\n  }\n\n  // step 4\n  if (enableScriptProcessing) {\n    const fn = new window.AsyncFunction(\n      'config',\n      `${originalProfile.script.code}; return await onGenerate(config)`,\n    )\n    try {\n      config = await fn(config)\n    } catch (error: any) {\n      throw error.message || error\n    }\n\n    if (typeof config !== 'object') {\n      throw 'Wrong result'\n    }\n  }\n\n  return config\n}\n\nexport const generateConfigFile = async (\n  profile: IProfile,\n  beforeWrite: (config: any) => Promise<any>,\n) => {\n  const header = `DO NOT EDIT - Generated by ${APP_TITLE}`\n\n  const _config = await generateConfig(profile)\n  const config = await beforeWrite(_config)\n\n  config.log.disabled = false\n  config.log.output = ''\n  if (![LogLevel.Trace, LogLevel.Debug, LogLevel.Info].includes(config.log.level)) {\n    config.log.level = LogLevel.Info\n  }\n\n  config.experimental.cache_file.path = 'cache.db'\n\n  await WriteFile(CoreConfigFilePath, JSON.stringify({ $schema: header, ...config }, null, 2))\n}\n"
  },
  {
    "path": "frontend/src/utils/helper.ts",
    "content": "import { deleteConnection, getConnections, useProxy } from '@/api/kernel'\nimport { AbsolutePath, Exec, ExitApp, ReadFile, WindowReloadApp, WriteFile } from '@/bridge'\nimport { CoreWorkingDirectory } from '@/constant/kernel'\nimport { RulesetFormat } from '@/enums/kernel'\nimport i18n from '@/lang'\nimport {\n  type ProxyType,\n  useAppSettingsStore,\n  useAppStore,\n  useEnvStore,\n  useKernelApiStore,\n  usePluginsStore,\n  useRulesetsStore,\n} from '@/stores'\nimport { ignoredError, message, confirm } from '@/utils'\n\n// Permissions Helper\nexport const SwitchPermissions = async (enable: boolean) => {\n  const { appPath } = useEnvStore().env\n  const args = enable\n    ? [\n        'add',\n        'HKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Layers',\n        '/v',\n        appPath,\n        '/t',\n        'REG_SZ',\n        '/d',\n        'RunAsAdmin',\n        '/f',\n      ]\n    : [\n        'delete',\n        'HKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Layers',\n        '/v',\n        appPath,\n        '/f',\n      ]\n  await Exec('reg', args, { Convert: true })\n}\n\nexport const CheckPermissions = async () => {\n  const { appPath } = useEnvStore().env\n  try {\n    const out = await Exec(\n      'reg',\n      [\n        'query',\n        'HKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Layers',\n        '/v',\n        appPath,\n        '/t',\n        'REG_SZ',\n      ],\n      { Convert: true },\n    )\n    return out.includes('RunAsAdmin')\n  } catch {\n    return false\n  }\n}\n\nexport const GrantTUNPermission = async (path: string) => {\n  const { os } = useEnvStore().env\n  const absPath = await AbsolutePath(path)\n  if (os === 'darwin') {\n    const osaScript = `chown root:admin ${absPath}\\nchmod +sx ${absPath}`\n    const bashScript = `osascript -e 'do shell script \"${osaScript}\" with administrator privileges'`\n    await Exec('bash', ['-c', bashScript])\n  } else if (os === 'linux') {\n    await Exec('pkexec', [\n      'setcap',\n      'cap_net_bind_service,cap_net_admin,cap_dac_override=+ep',\n      absPath,\n    ])\n  }\n}\n\nexport const RunWithPowerShell = async (\n  path: string,\n  args: string[] = [],\n  options: { admin?: boolean; hidden?: boolean; wait?: boolean },\n) => {\n  const { admin = false, hidden = false, wait = true, ...others } = options\n  const psArgs: string[] = []\n  let command = `Start-Process -FilePath \"${path}\"`\n  if (args.length > 0) {\n    const argList = args.map((a) => `\"${a.replace(/\"/g, '\"\"')}\"`).join(',')\n    command += ` -ArgumentList ${argList}`\n  }\n  if (admin) {\n    command += ' -Verb RunAs'\n  }\n  if (hidden) {\n    command += ' -WindowStyle Hidden'\n  }\n  if (wait) {\n    command += ' -Wait'\n  }\n  psArgs.push('-NoProfile', '-Command', command)\n  await Exec('powershell', psArgs, { Convert: true, ...others })\n}\n\n// SystemProxy Helper\nexport const SetSystemProxy = async (\n  enable: boolean,\n  server: string,\n  proxyType: ProxyType = 'mixed',\n  bypass = '',\n) => {\n  const { os } = useEnvStore().env\n\n  const handler = {\n    windows: setWindowsSystemProxy,\n    darwin: setDarwinSystemProxy,\n    linux: setLinuxSystemProxy,\n  }[os]\n\n  await handler?.(server, enable, proxyType, bypass)\n}\n\nasync function setWindowsSystemProxy(\n  server: string,\n  enabled: boolean,\n  proxyType: ProxyType,\n  bypass: string,\n) {\n  const p1 = ignoredError(Exec, 'reg', [\n    'add',\n    'HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings',\n    '/v',\n    'ProxyEnable',\n    '/t',\n    'REG_DWORD',\n    '/d',\n    enabled ? '1' : '0',\n    '/f',\n  ])\n\n  const p2 = ignoredError(Exec, 'reg', [\n    'add',\n    'HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings',\n    '/v',\n    'ProxyServer',\n    '/d',\n    enabled ? (proxyType === 'socks' ? 'socks=' + server : server) : '',\n    '/f',\n  ])\n\n  const p3 = ignoredError(Exec, 'reg', [\n    'add',\n    'HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings',\n    '/v',\n    'ProxyOverride',\n    '/d',\n    bypass\n      .split(';')\n      .map((v) => v.trim())\n      .filter(Boolean)\n      .join(';'),\n    '/f',\n  ])\n\n  await Promise.all([p1, p2, p3])\n}\n\nasync function setDarwinSystemProxy(\n  server: string,\n  enabled: boolean,\n  proxyType: ProxyType,\n  bypass: string,\n) {\n  async function _set(device: string) {\n    const state = enabled ? 'on' : 'off'\n\n    const httpState = ['mixed', 'http'].includes(proxyType) ? state : 'off'\n    const socksState = ['mixed', 'socks'].includes(proxyType) ? state : 'off'\n\n    const p1 = ignoredError(Exec, 'networksetup', ['-setwebproxystate', device, httpState])\n    const p2 = ignoredError(Exec, 'networksetup', ['-setsecurewebproxystate', device, httpState])\n    const p3 = ignoredError(Exec, 'networksetup', [\n      '-setsocksfirewallproxystate',\n      device,\n      socksState,\n    ])\n    const p4 = ignoredError(Exec, 'networksetup', [\n      '-setproxybypassdomains',\n      device,\n      ...bypass\n        .split(';')\n        .map((v) => v.trim())\n        .filter(Boolean),\n    ])\n\n    const [serverName, serverPort] = server.split(':') as [string, string]\n\n    const promises = [p1, p2, p3, p4]\n    if (httpState === 'on') {\n      const p1 = ignoredError(Exec, 'networksetup', [\n        '-setwebproxy',\n        device,\n        serverName,\n        serverPort,\n      ])\n      const p2 = ignoredError(Exec, 'networksetup', [\n        '-setsecurewebproxy',\n        device,\n        serverName,\n        serverPort,\n      ])\n      promises.push(p1, p2)\n    }\n    if (socksState === 'on') {\n      const p1 = ignoredError(Exec, 'networksetup', [\n        '-setsocksfirewallproxy',\n        device,\n        serverName,\n        serverPort,\n      ])\n      promises.push(p1)\n    }\n\n    await Promise.all(promises)\n  }\n  const p1 = _set('Ethernet')\n  const p2 = _set('Wi-Fi')\n  await Promise.all([p1, p2])\n}\n\nasync function setLinuxSystemProxy(\n  server: string,\n  enabled: boolean,\n  proxyType: ProxyType,\n  bypass: string,\n) {\n  const [serverName, serverPort] = server.split(':') as [string, string]\n  const httpEnabled = enabled && ['mixed', 'http'].includes(proxyType)\n  const socksEnabled = enabled && ['mixed', 'socks'].includes(proxyType)\n\n  const desktop = (await Exec('sh', ['-c', 'echo $XDG_CURRENT_DESKTOP'])).trim()\n  if (desktop.includes('KDE')) {\n    const p1 = ignoredError(Exec, 'kwriteconfig5', [\n      '--file',\n      'kioslaverc',\n      '--group',\n      'Proxy Settings',\n      '--key',\n      'ProxyType',\n      enabled ? '1' : '0',\n    ])\n    const p2 = ignoredError(Exec, 'kwriteconfig5', [\n      '--file',\n      'kioslaverc',\n      '--group',\n      'Proxy Settings',\n      '--key',\n      'httpProxy',\n      httpEnabled ? `http://${server}` : '',\n    ])\n    const p3 = ignoredError(Exec, 'kwriteconfig5', [\n      '--file',\n      'kioslaverc',\n      '--group',\n      'Proxy Settings',\n      '--key',\n      'httpsProxy',\n      httpEnabled ? `http://${server}` : '',\n    ])\n    const p4 = ignoredError(Exec, 'kwriteconfig5', [\n      '--file',\n      'kioslaverc',\n      '--group',\n      'Proxy Settings',\n      '--key',\n      'socksProxy',\n      socksEnabled ? `socks://${server}` : '',\n    ])\n    const p5 = ignoredError(Exec, 'kwriteconfig5', [\n      '--file',\n      'kioslaverc',\n      '--group',\n      'Proxy Settings',\n      '--key',\n      'NoProxyFor',\n      bypass\n        .split(';')\n        .map((v) => v.trim())\n        .filter(Boolean)\n        .join(','),\n    ])\n    await Promise.all([p1, p2, p3, p4, p5])\n  } else if (['GNOME', 'XFCE'].includes(desktop)) {\n    const p1 = ignoredError(Exec, 'gsettings', [\n      'set',\n      'org.gnome.system.proxy',\n      'mode',\n      enabled ? 'manual' : 'none',\n    ])\n    const p2 = ignoredError(Exec, 'gsettings', [\n      'set',\n      'org.gnome.system.proxy.http',\n      'host',\n      httpEnabled ? serverName : '',\n    ])\n    const p3 = ignoredError(Exec, 'gsettings', [\n      'set',\n      'org.gnome.system.proxy.http',\n      'port',\n      httpEnabled ? serverPort : '0',\n    ])\n    const p4 = ignoredError(Exec, 'gsettings', [\n      'set',\n      'org.gnome.system.proxy.https',\n      'host',\n      httpEnabled ? serverName : '',\n    ])\n    const p5 = ignoredError(Exec, 'gsettings', [\n      'set',\n      'org.gnome.system.proxy.https',\n      'port',\n      httpEnabled ? serverPort : '0',\n    ])\n    const p6 = ignoredError(Exec, 'gsettings', [\n      'set',\n      'org.gnome.system.proxy.socks',\n      'host',\n      socksEnabled ? serverName : '',\n    ])\n    const p7 = ignoredError(Exec, 'gsettings', [\n      'set',\n      'org.gnome.system.proxy.socks',\n      'port',\n      socksEnabled ? serverPort : '0',\n    ])\n    const p8 = ignoredError(Exec, 'gsettings', [\n      'set',\n      'org.gnome.system.proxy',\n      'ignore-hosts',\n      `[${bypass\n        .split(';')\n        .map((v) => v.trim())\n        .filter(Boolean)\n        .map((v) => `'${v}'`)\n        .join(',')}]`,\n    ])\n    await Promise.all([p1, p2, p3, p4, p5, p6, p7, p8])\n  }\n}\n\nexport const GetSystemProxy = async () => {\n  const { os } = useEnvStore().env\n  try {\n    if (os === 'windows') {\n      const out1 = await Exec('reg', [\n        'query',\n        'HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings',\n        '/v',\n        'ProxyEnable',\n        '/t',\n        'REG_DWORD',\n      ])\n\n      if (/REG_DWORD\\s+0x0/.test(out1)) return ''\n\n      const out2 = await Exec('reg', [\n        'query',\n        'HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings',\n        '/v',\n        'ProxyServer',\n        '/t',\n        'REG_SZ',\n      ])\n\n      const regex = /ProxyServer\\s+REG_SZ\\s+(\\S+)/\n      const match = out2.match(regex)\n\n      return match ? (match?.[1]?.startsWith('socks') ? match[1] : 'http://' + match[1]) : ''\n    }\n\n    if (os === 'darwin') {\n      const out = await Exec('scutil', ['--proxy'])\n      const regex =\n        /(?:HTTPEnable|HTTPPort|HTTPProxy|SOCKSEnable|SOCKSPort|SOCKSProxy)\\s*:\\s*([^}\\n]+)/g\n      const map: Record<string, any> = {}\n      let match\n\n      while ((match = regex.exec(out)) !== null) {\n        const value = match[1]?.trim()\n        const key = (match[0].split(':') as [string, string])[0].trim()\n        map[key] = value\n      }\n\n      if (map['HTTPEnable'] === '1') {\n        return 'http://' + map['HTTPProxy'] + ':' + map['HTTPPort']\n      }\n\n      if (map['SOCKSEnable'] === '1') {\n        return 'socks5://' + map['SOCKSProxy'] + ':' + map['SOCKSPort']\n      }\n\n      return ''\n    }\n\n    if (os === 'linux') {\n      const desktop = (await Exec('sh', ['-c', 'echo $XDG_CURRENT_DESKTOP'])).trim()\n      if (desktop.includes('KDE')) {\n        const out = await Exec('kreadconfig5', [\n          '--file',\n          'kioslaverc',\n          '--group',\n          'Proxy Settings',\n          '--key',\n          'ProxyType',\n        ])\n        if (out.includes('1')) {\n          const out1 = await Exec('kreadconfig5', [\n            '--file',\n            'kioslaverc',\n            '--group',\n            'Proxy Settings',\n            '--key',\n            'httpProxy',\n          ])\n          const http = out1.replace(/['\"\\n]/g, '')\n          if (http) {\n            return http.replace(' ', ':')\n          }\n          const out2 = await Exec('kreadconfig5', [\n            '--file',\n            'kioslaverc',\n            '--group',\n            'Proxy Settings',\n            '--key',\n            'socksProxy',\n          ])\n          const socks = out2.replace(/['\"\\n]/g, '')\n          if (socks) {\n            return socks.replace(' ', ':')\n          }\n        }\n      } else if (['GNOME', 'XFCE'].includes(desktop)) {\n        const out = await Exec('gsettings', ['get', 'org.gnome.system.proxy', 'mode'])\n        if (out.includes('none')) {\n          return ''\n        }\n\n        if (out.includes('manual')) {\n          const out1 = await Exec('gsettings', ['get', 'org.gnome.system.proxy.http', 'host'])\n          const out2 = await Exec('gsettings', ['get', 'org.gnome.system.proxy.http', 'port'])\n          const httpHost = out1.replace(/['\"\\n]/g, '')\n          const httpPort = out2.replace(/['\"\\n]/g, '')\n          if (httpHost && httpPort !== '0') {\n            return 'http://' + httpHost + ':' + httpPort\n          }\n\n          const out3 = await Exec('gsettings', ['get', 'org.gnome.system.proxy.socks', 'host'])\n          const out4 = await Exec('gsettings', ['get', 'org.gnome.system.proxy.socks', 'port'])\n          const socksHost = out3.replace(/['\"\\n]/g, '')\n          const socksPort = out4.replace(/['\"\\n]/g, '')\n          if (socksHost && socksPort !== '0') {\n            return 'socks5://' + socksHost + ':' + socksPort\n          }\n        }\n      }\n    }\n  } catch (error) {\n    console.log('error', error)\n  }\n  return ''\n}\n\nexport const GetSystemProxyBypass = async () => {\n  const { os } = useEnvStore().env\n\n  if (os === 'windows') {\n    const out = await ignoredError(Exec, 'reg', [\n      'query',\n      'HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings',\n      '/v',\n      'ProxyOverride',\n    ])\n    if (!out) return ''\n    return out.match(/ProxyOverride\\s+REG_SZ\\s+(\\S+)/)?.[1] || ''\n  }\n\n  if (os === 'darwin') {\n    async function _get(device: string) {\n      const out = await ignoredError(Exec, 'networksetup', ['-getproxybypassdomains', device])\n      if (!out) return []\n      return out.trim().split('\\n').filter(Boolean)\n    }\n    const res = await Promise.all([_get('Ethernet'), _get('Wi-Fi')])\n    return res.flat().join(';')\n  }\n\n  if (os === 'linux') {\n    const desktop = (await Exec('sh', ['-c', 'echo $XDG_CURRENT_DESKTOP'])).trim()\n    if (desktop.includes('KDE')) {\n      const out = await ignoredError(Exec, 'kreadconfig5', [\n        '--file',\n        'kioslaverc',\n        '--group',\n        'Proxy Settings',\n        '--key',\n        'NoProxyFor',\n      ])\n      if (!out) return ''\n      return out\n        .trim()\n        .split(',')\n        .map((v) => v.trim())\n        .join(';')\n    } else if (['GNOME', 'XFCE'].includes(desktop)) {\n      const out = await ignoredError(Exec, 'gsettings', [\n        'get',\n        'org.gnome.system.proxy',\n        'ignore-hosts',\n      ])\n      if (!out) return ''\n      const arrStart = out.indexOf('[')\n      const arrStr = arrStart >= 0 ? out.slice(arrStart) : out\n      const jsonLike = arrStr.replace(/'/g, '\"')\n      const arr = (await ignoredError(JSON.parse, jsonLike)) ?? []\n      if (!Array.isArray(arr)) return ''\n      return arr.join(';')\n    }\n  }\n  return ''\n}\n\nconst proxy_cache: { proxyPromise: Promise<string> | null; lastAccessTime: number } = {\n  proxyPromise: null,\n  lastAccessTime: 0,\n}\n\nexport const GetSystemOrKernelProxy = async () => {\n  if (useKernelApiStore().running) {\n    const kernelProxy = useKernelApiStore().getProxyPort()\n    if (kernelProxy !== undefined) {\n      if (kernelProxy.proxyType === 'socks') {\n        return `socks5://127.0.0.1:${kernelProxy.port}`\n      }\n      return `http://127.0.0.1:${kernelProxy.port}`\n    }\n  }\n\n  if (proxy_cache.proxyPromise && Date.now() - proxy_cache.lastAccessTime < 1000) {\n    return proxy_cache.proxyPromise\n  }\n\n  proxy_cache.lastAccessTime = Date.now()\n  proxy_cache.proxyPromise = GetSystemProxy()\n  return proxy_cache.proxyPromise\n}\n\nexport const QuerySchTask = async (taskName: string) => {\n  await Exec('Schtasks', ['/Query', '/TN', taskName, '/XML'], { Convert: true })\n}\n\nexport const CreateSchTask = async (taskName: string, xmlPath: string) => {\n  const fn = useEnvStore().env.isPrivileged ? Exec : RunWithPowerShell\n  await fn('SchTasks', ['/Create', '/F', '/TN', taskName, '/XML', xmlPath], {\n    admin: true,\n    hidden: true,\n  })\n}\n\nexport const DeleteSchTask = async (taskName: string) => {\n  const fn = useEnvStore().env.isPrivileged ? Exec : RunWithPowerShell\n  await fn('SchTasks', ['/Delete', '/F', '/TN', taskName], { admin: true, hidden: true })\n}\n\n// Others\nexport const handleUseProxy = async (group: any, proxy: any) => {\n  if (group.type !== 'Selector' || group.now === proxy.name) return\n  const promises: Promise<null>[] = []\n  const appSettings = useAppSettingsStore()\n  const kernelApiStore = useKernelApiStore()\n  if (appSettings.app.kernel.autoClose) {\n    const { connections } = await getConnections()\n    promises.push(\n      ...(connections || [])\n        .filter((v) => v.chains.includes(group.name))\n        .map((v) => deleteConnection(v.id)),\n    )\n  }\n  await useProxy(encodeURIComponent(group.name), proxy.name)\n  await Promise.all(promises)\n  await kernelApiStore.refreshProviderProxies()\n}\n\nexport const handleChangeMode = async (mode: 'direct' | 'global' | 'rule') => {\n  const kernelApiStore = useKernelApiStore()\n\n  if (mode === kernelApiStore.config.mode) return\n\n  kernelApiStore.updateConfig('mode', mode)\n\n  const { connections } = await getConnections()\n  const promises = (connections || []).map((v) => deleteConnection(v.id))\n  await Promise.all(promises)\n}\n\nexport const addToRuleSet = async (\n  id: 'direct' | 'reject' | 'proxy',\n  payloads: Record<string, any>[],\n) => {\n  const path = `data/rulesets/${id}.json`\n\n  const rulesetsStoe = useRulesetsStore()\n  let ruleset = rulesetsStoe.getRulesetById(id)\n  if (!ruleset) {\n    ruleset = {\n      id,\n      tag: id,\n      updateTime: 0,\n      type: 'Manual',\n      format: RulesetFormat.Source,\n      url: '',\n      path,\n      count: 0,\n      disabled: false,\n    }\n    await rulesetsStoe.addRuleset(ruleset)\n  }\n\n  const content = (await ignoredError(ReadFile, path)) || '{ \"version\": 1, \"rules\": [] }'\n  const { rules = [] } = JSON.parse(content)\n  rules[0] = rules[0] || {}\n  payloads.forEach((payload) => {\n    if (payload.domain) {\n      rules[0].domain = [...new Set((rules[0].domain || []).concat(payload.domain))]\n    } else if (payload.ip_cidr) {\n      rules[0].ip_cidr = [...new Set((rules[0].ip_cidr || []).concat(payload.ip_cidr))]\n    } else if (payload.process_path) {\n      rules[0].process_path = [\n        ...new Set((rules[0].process_path || []).concat(payload.process_path)),\n      ]\n    } else if (payload.domain_suffix) {\n      rules[0].domain_suffix = [\n        ...new Set((rules[0].domain_suffix || []).concat(payload.domain_suffix)),\n      ]\n    }\n  })\n  await WriteFile(path, JSON.stringify({ version: 1, rules }, null, 2))\n  await rulesetsStoe.updateRuleset(id)\n}\n\nexport const reloadApp = async () => {\n  const { t } = i18n.global\n  const appStore = useAppStore()\n  const pluginsStore = usePluginsStore()\n\n  appStore.isAppReloading = true\n\n  let timedout = false\n  const { destroy } = message.info('titlebar.reloadPending', 10 * 60 * 1000)\n\n  const timeoutId = setTimeout(async () => {\n    timedout = true\n    appStore.isAppReloading = false\n    destroy()\n    confirm('Warning', t('titlebar.reloadTimeout')).then(WindowReloadApp)\n  }, 10_000)\n\n  try {\n    await pluginsStore.onReloadTrigger()\n    if (!timedout) {\n      clearTimeout(timeoutId)\n      WindowReloadApp()\n    }\n  } catch (err: any) {\n    clearTimeout(timeoutId)\n    confirm('Error', t('titlebar.reloadError', { reason: err })).then(WindowReloadApp)\n  }\n\n  appStore.isAppReloading = false\n  destroy()\n}\n\nexport const exitApp = async () => {\n  const { t } = i18n.global\n  const appStore = useAppStore()\n  const envStore = useEnvStore()\n  const pluginsStore = usePluginsStore()\n  const appSettings = useAppSettingsStore()\n  const kernelApiStore = useKernelApiStore()\n\n  appStore.isAppExiting = true\n\n  let timedout = false\n  const { destroy } = message.info('titlebar.exitPending', 10 * 60 * 1000)\n\n  const timeoutId = setTimeout(async () => {\n    timedout = true\n    appStore.isAppExiting = false\n    destroy()\n    confirm('Warning', t('titlebar.exitTimeout')).then(ExitApp)\n  }, 10_000)\n\n  try {\n    if (kernelApiStore.running && appSettings.app.closeKernelOnExit) {\n      await kernelApiStore.stopCore()\n      if (appSettings.app.autoSetSystemProxy) {\n        await envStore.clearSystemProxy()\n      }\n    }\n    await pluginsStore.onShutdownTrigger()\n    if (!timedout) {\n      clearTimeout(timeoutId)\n      ExitApp()\n    }\n  } catch (err: any) {\n    clearTimeout(timeoutId)\n    confirm('Error', t('titlebar.exitError', { reason: err })).then(ExitApp)\n  }\n\n  appStore.isAppExiting = false\n  destroy()\n}\n\nexport const getKernelFileName = (isAlpha = false) => {\n  const envStore = useEnvStore()\n  const { os } = envStore.env\n  const fileSuffix = { windows: '.exe', linux: '', darwin: '' }[os]\n  const latest = isAlpha ? '-latest' : ''\n  return `sing-box${latest}${fileSuffix}`\n}\n\nexport const getKernelAssetFileName = (version: string) => {\n  const envStore = useEnvStore()\n  const { os, arch } = envStore.env\n  const suffix = { windows: '.zip', linux: '.tar.gz', darwin: '.tar.gz' }[os]\n  return `sing-box-${version}-${os}-${arch}${suffix}`\n}\n\nexport const processMagicVariables = (str: string) => {\n  const { env } = useEnvStore()\n  let result = str\n  Object.entries({\n    $APP_BASE_PATH: env.basePath,\n    $CORE_BASE_PATH: CoreWorkingDirectory,\n  }).forEach(([source, target]) => {\n    result = result.replaceAll(source, target)\n  })\n  return result\n}\n\nexport const getKernelRuntimeEnv = (isAlpha = false) => {\n  const appSettings = useAppSettingsStore()\n  const { env } = isAlpha ? appSettings.app.kernel.alpha : appSettings.app.kernel.main\n  return Object.entries(env).reduce((p, [key, value]) => {\n    p[key] = processMagicVariables(value)\n    return p\n  }, {} as Recordable)\n}\n\nexport const getKernelRuntimeArgs = (isAlpha = false) => {\n  const appSettings = useAppSettingsStore()\n  const { args } = isAlpha ? appSettings.app.kernel.alpha : appSettings.app.kernel.main\n  return args.map((arg) => processMagicVariables(arg))\n}\n"
  },
  {
    "path": "frontend/src/utils/index.ts",
    "content": "export * from './env'\nexport * from './format'\nexport * from './generator'\nexport * from './restorer'\nexport * from './is'\nexport * from './others'\nexport * from './helper'\nexport * from './tray'\nexport * from './completion'\nexport * from './interaction'\nexport * from './eventBus'\nexport * from './migration'\n"
  },
  {
    "path": "frontend/src/utils/interaction.ts",
    "content": "import { render, h, type VNode, nextTick } from 'vue'\n\nimport i18n from '@/lang'\nimport { APP_TITLE, sampleID } from '@/utils'\n\nimport ConfirmComp from '@/components/Confirm/index.vue'\nimport MessageComp from '@/components/Message/index.vue'\nimport { useModal } from '@/components/Modal'\nimport PickerComp from '@/components/Picker/index.vue'\nimport PromptComp from '@/components/Prompt/index.vue'\n\nimport type { ConfirmOptions } from '@/components/Confirm/index.vue'\nimport type { Props as InputProps } from '@/components/Input/index.vue'\nimport type { MessageIcon } from '@/components/Message/index.vue'\nimport type { Props as ModalProps, Slots as ModalSlots } from '@/components/Modal/index.vue'\nimport type { PickerItem } from '@/components/Picker/index.vue'\n\nconst ContainerCssText = `\n    position: fixed;\n    z-index: 99999;\n    top: 84px;\n    left: 0;\n    right: 0;\n    display: flex;\n    justify-content: center;\n    max-height: 70%;\n`\n\ninterface MessageInstance {\n  dom: HTMLDivElement\n  vnode: VNode\n  timer: number\n}\n\nconst bindAppContext = (vnode: VNode) => {\n  vnode.appContext = window.appInstance._context\n}\n\nclass Message {\n  public container: HTMLElement\n  public instances: Record<string, MessageInstance>\n\n  constructor() {\n    const ID = APP_TITLE + '-toast'\n    this.container = document.getElementById(ID) || document.createElement('div')\n    this.container.id = ID\n    this.container.style.cssText = `\n        position: fixed;\n        z-index: 999999;\n        top: 80px;\n        left: 50%;\n        transform: translateX(-50%);\n    `\n    document.body.appendChild(this.container)\n    this.instances = {}\n  }\n\n  private buildMessage = (icon: MessageIcon) => {\n    return (content: string, duration = 3_000, onClose?: () => void) => {\n      const id = sampleID()\n      const dom = document.createElement('div')\n\n      const onMouseEnter = () => clearTimeout(this.instances[id]!.timer)\n      const onMouseLeave = () => (this.instances[id]!.timer = setTimeout(onDestroy, duration))\n\n      const onDestroy = () => {\n        dom.removeEventListener('mouseenter', onMouseEnter)\n        dom.removeEventListener('mouseleave', onMouseLeave)\n        this.destroy(id)\n      }\n\n      const initInstance = () => {\n        dom.style.cssText = 'display: flex; align-items: center; justify-content: center;'\n\n        const vnode = h(MessageComp, {\n          icon,\n          content,\n          onClose: () => {\n            onClose?.()\n            onDestroy()\n          },\n        })\n        bindAppContext(vnode)\n\n        this.instances[id] = {\n          dom,\n          vnode,\n          timer: setTimeout(onDestroy, duration),\n        }\n\n        dom.addEventListener('mouseenter', onMouseEnter)\n        dom.addEventListener('mouseleave', onMouseLeave)\n\n        this.container.appendChild(dom)\n        render(vnode, dom)\n      }\n\n      initInstance()\n\n      return {\n        id,\n        info: (content: string) => this.update(id, content, 'info'),\n        warn: (content: string) => this.update(id, content, 'warn'),\n        error: (content: string) => this.update(id, content, 'error'),\n        success: (content: string) => this.update(id, content, 'success'),\n        update: (content: string, icon?: MessageIcon) => this.update(id, content, icon),\n        destroy: onDestroy,\n      }\n    }\n  }\n\n  public info = this.buildMessage('info')\n  public warn = this.buildMessage('warn')\n  public error = this.buildMessage('error')\n  public success = this.buildMessage('success')\n\n  public update = (id: string, content: string, icon?: MessageIcon) => {\n    const instance = this.instances[id]\n    if (instance) {\n      icon && (instance.vnode.component!.props.icon = icon)\n      content && (instance.vnode.component!.props.content = content)\n    }\n  }\n\n  public destroy = (id: string) => {\n    const instance = this.instances[id]\n    if (instance) {\n      render(null, instance.dom)\n      instance.dom.remove()\n      clearTimeout(instance.timer)\n      delete this.instances[id]\n    }\n  }\n}\n\nclass Picker {\n  constructor() {}\n\n  public single = <T>(title: string, options: PickerItem<T>[], initialValue: T[] = []) => {\n    return this.buildPicker('single', title, options, initialValue)\n  }\n\n  public multi = <T>(title: string, options: PickerItem<T>[], initialValue: T[] = []) => {\n    return this.buildPicker('multi', title, options, initialValue)\n  }\n\n  private buildPicker = <ValueType, PickerType extends 'single' | 'multi'>(\n    type: PickerType,\n    title: string,\n    options: PickerItem<ValueType>[],\n    initialValue: ValueType[],\n  ): Promise<PickerType extends 'single' ? ValueType : ValueType[]> => {\n    return new Promise((resolve, reject) => {\n      const { t } = i18n.global\n      const dom = document.createElement('div')\n      dom.style.cssText = ContainerCssText\n      const vnode = h(PickerComp<ValueType, PickerType>, {\n        type,\n        title,\n        options,\n        initialValue,\n        onConfirm: resolve,\n        onCancel: () => reject(t('common.canceled')),\n        onFinish: () => {\n          render(null, dom)\n          dom.remove()\n        },\n      })\n      bindAppContext(vnode)\n      document.body.appendChild(dom)\n      render(vnode, dom)\n    })\n  }\n}\n\nconst buildConfirm = (\n  title: string,\n  message: string,\n  options: ConfirmOptions = { type: 'text' },\n  cancel = true,\n) => {\n  return new Promise((resolve, reject) => {\n    const { t } = i18n.global\n    const dom = document.createElement('div')\n    dom.style.cssText = ContainerCssText\n    const vnode = h(ConfirmComp, {\n      title,\n      message,\n      options,\n      cancel,\n      onConfirm: resolve,\n      onCancel: () => reject(t('common.canceled')),\n      onFinish: () => {\n        render(null, dom)\n        dom.remove()\n      },\n    })\n    bindAppContext(vnode)\n    document.body.appendChild(dom)\n    render(vnode, dom)\n  })\n}\n\nexport const prompt = <T>(\n  title: string,\n  initialValue: string | number = '',\n  props: Partial<InputProps> = {},\n) => {\n  const { t } = i18n.global\n\n  return new Promise<T>((resolve, reject) => {\n    const dom = document.createElement('div')\n    dom.style.cssText = ContainerCssText\n    const vnode = h(PromptComp, {\n      title,\n      initialValue,\n      props,\n      onSubmit: resolve,\n      onCancel: () => reject(t('common.canceled')),\n      onFinish: () => {\n        render(null, dom)\n        dom.remove()\n      },\n    })\n    bindAppContext(vnode)\n    document.body.appendChild(dom)\n    render(vnode, dom)\n  })\n}\n\nexport const alert = (\n  title: string,\n  message: string,\n  options: ConfirmOptions = { type: 'text' },\n) => {\n  return buildConfirm(title, message, options, false)\n}\n\nexport const confirm = (\n  title: string,\n  message: string,\n  options: ConfirmOptions = { type: 'text' },\n) => {\n  return buildConfirm(title, message, options)\n}\n\nexport const modal = (options: ModalProps = {}, slots: ModalSlots = {}) => {\n  const [Modal, api] = useModal(options, slots)\n  const vnode = h(Modal)\n  bindAppContext(vnode)\n\n  const container = document.createElement('div')\n  document.body.appendChild(container)\n  render(vnode, container)\n\n  const destroy = () => {\n    api.close()\n    nextTick(() => {\n      render(null, container)\n      container.remove()\n    })\n  }\n\n  const powerApi = { ...api, destroy }\n  return powerApi\n}\n\nexport const picker = new Picker()\n\nexport const message = new Message()\n"
  },
  {
    "path": "frontend/src/utils/is.ts",
    "content": "import { Cron } from 'croner'\nimport { parse } from 'yaml'\n\nimport { normalizeBase64 } from './others'\n\nexport const isValidBase64 = (str: string) => {\n  if (typeof str !== 'string') return false\n  if (str === '' || str.trim() === '') {\n    return false\n  }\n\n  // Accept URL-safe base64 and ignore line breaks/spaces in subscription responses.\n  const normalized = normalizeBase64(str)\n  try {\n    atob(normalized)\n    return true\n  } catch {\n    return false\n  }\n}\n\nexport const isValidSubYAML = (str: string) => {\n  if (typeof str !== 'string') return false\n  try {\n    const { proxies } = parse(str)\n    return !!proxies\n  } catch {\n    return false\n  }\n}\n\nexport const isValidSubJson = (str: string) => {\n  if (typeof str !== 'string') return false\n  try {\n    const { outbounds } = JSON.parse(str)\n    return !!outbounds\n  } catch {\n    return false\n  }\n}\n\nexport const isValidPaylodYAML = (str: string) => {\n  try {\n    const { payload } = parse(str)\n    return !!payload\n  } catch {\n    return false\n  }\n}\n\nexport const isValidRulesJson = (str: string) => {\n  try {\n    const { rules } = JSON.parse(str)\n    return !!rules\n  } catch {\n    return false\n  }\n}\n\nexport const isValidIPv4 = (ip: string) =>\n  /^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$/.test(ip)\n\nexport const isValidIPv6 = (ip: string) =>\n  /^([\\da-fA-F]{1,4}:){6}((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)|::([\\da−fA−F]1,4:)0,4((25[0−5]|2[0−4]\\d|[01]?\\d\\d?)\\.)3(25[0−5]|2[0−4]\\d|[01]?\\d\\d?)|::([\\da−fA−F]1,4:)0,4((25[0−5]|2[0−4]\\d|[01]?\\d\\d?)\\.)3(25[0−5]|2[0−4]\\d|[01]?\\d\\d?)|^([\\da-fA-F]{1,4}:):([\\da-fA-F]{1,4}:){0,3}((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)|([\\da−fA−F]1,4:)2:([\\da−fA−F]1,4:)0,2((25[0−5]|2[0−4]\\d|[01]?\\d\\d?)\\.)3(25[0−5]|2[0−4]\\d|[01]?\\d\\d?)|([\\da−fA−F]1,4:)2:([\\da−fA−F]1,4:)0,2((25[0−5]|2[0−4]\\d|[01]?\\d\\d?)\\.)3(25[0−5]|2[0−4]\\d|[01]?\\d\\d?)|^([\\da-fA-F]{1,4}:){3}:([\\da-fA-F]{1,4}:){0,1}((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)|([\\da−fA−F]1,4:)4:((25[0−5]|2[0−4]\\d|[01]?\\d\\d?)\\.)3(25[0−5]|2[0−4]\\d|[01]?\\d\\d?)|([\\da−fA−F]1,4:)4:((25[0−5]|2[0−4]\\d|[01]?\\d\\d?)\\.)3(25[0−5]|2[0−4]\\d|[01]?\\d\\d?)|^([\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}|:((:[\\da−fA−F]1,4)1,6|:)|:((:[\\da−fA−F]1,4)1,6|:)|^[\\da-fA-F]{1,4}:((:[\\da-fA-F]{1,4}){1,5}|:)|([\\da−fA−F]1,4:)2((:[\\da−fA−F]1,4)1,4|:)|([\\da−fA−F]1,4:)2((:[\\da−fA−F]1,4)1,4|:)|^([\\da-fA-F]{1,4}:){3}((:[\\da-fA-F]{1,4}){1,3}|:)|([\\da−fA−F]1,4:)4((:[\\da−fA−F]1,4)1,2|:)|([\\da−fA−F]1,4:)4((:[\\da−fA−F]1,4)1,2|:)|^([\\da-fA-F]{1,4}:){5}:([\\da-fA-F]{1,4})?|([\\da−fA−F]1,4:)6:|([\\da−fA−F]1,4:)6:/.test(\n    ip,\n  )\n\nexport const isValidJson = (str: string) => {\n  try {\n    return !!JSON.parse(str)\n  } catch {\n    return false\n  }\n}\n\nexport const isNumber = (v: any) => typeof v === 'number'\n\nexport const isValidCron = (pattern: string) => {\n  try {\n    const instance = new Cron(pattern, { paused: true })\n    return { ok: true, reason: null, instance: instance }\n  } catch (error: any) {\n    return { ok: false, reason: error.message || error, instance: null }\n  }\n}\n"
  },
  {
    "path": "frontend/src/utils/migration.ts",
    "content": "export const migrateProfiles = async (profiles: IProfile[], save: () => Promise<string>) => {\n  let needSync = false\n\n  profiles.forEach((profile) => {\n    profile.dns.rules.forEach((rule) => {\n      if (typeof rule.enable === 'undefined') {\n        rule.enable = true\n        needSync = true\n      }\n    })\n    profile.route.rules.forEach((rule) => {\n      if (typeof rule.enable === 'undefined') {\n        rule.enable = true\n        needSync = true\n      }\n    })\n  })\n\n  if (needSync) await save()\n}\n"
  },
  {
    "path": "frontend/src/utils/others.ts",
    "content": "import { stringify } from 'yaml'\n\nimport { useAppSettingsStore, useEnvStore } from '@/stores'\nimport { APP_TITLE, APP_VERSION } from '@/utils'\n\nexport const deepClone = <T>(json: T): T => JSON.parse(JSON.stringify(json))\n\nexport const omit = <T extends object, K extends keyof T>(obj: T, props: K[]): Omit<T, K> => {\n  const result = {} as T\n  const omitSet = new Set(props)\n  for (const key in obj) {\n    if (Object.prototype.hasOwnProperty.call(obj, key)) {\n      if (!omitSet.has(key as unknown as K)) {\n        result[key] = obj[key]\n      }\n    }\n  }\n  return result as Omit<T, K>\n}\n\nexport const omitArray = <T, K extends keyof T>(arr: T[], fields: K[]): Omit<T, K>[] => {\n  return arr.map((obj) => {\n    const item: Partial<T> = deepClone(obj)\n    fields.forEach((key) => {\n      delete item[key]\n    })\n    return item as Omit<T, K>\n  })\n}\nexport const debounce = (fn: (...args: any) => any, wait: number) => {\n  let timer: null | number = null\n  const _debuonce = function (...args: any) {\n    return new Promise((resolve, reject) => {\n      timer && clearTimeout(timer)\n      timer = setTimeout(async () => {\n        try {\n          await fn(...args)\n          resolve(null)\n        } catch (error) {\n          reject(error)\n        }\n      }, wait)\n    })\n  }\n  _debuonce.cancel = function () {\n    timer && clearTimeout(timer)\n    timer = null\n  }\n  return _debuonce\n}\n\nexport const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))\n\nexport const ignoredError = async <F extends (...args: any[]) => Promise<any>>(\n  fn: F,\n  ...args: Parameters<F>\n): Promise<ReturnType<F> | undefined> => {\n  try {\n    return await fn(...args)\n  } catch {\n    return undefined\n  }\n}\n\nexport const sampleID = () => 'ID_' + Math.random().toString(36).substring(2, 10)\n\nexport const generateSecureKey = (bits = 256) => {\n  const bytes = bits / 8\n  const array = new Uint8Array(bytes)\n  crypto.getRandomValues(array)\n  return Array.from(array)\n    .map((b) => b.toString(16).padStart(2, '0'))\n    .join('')\n}\n\nexport const getValue = <T = unknown>(obj: unknown, expr: string): T | undefined => {\n  return expr.split('.').reduce<unknown>((value, key) => {\n    if (value && typeof value === 'object') {\n      return (value as Record<string, unknown>)[key]\n    }\n    return undefined\n  }, obj) as T\n}\n\ntype IteratorFn<T, K> = (item: T, array: T[]) => Promise<K>\ntype PoolController = { pause: () => void; resume: () => void; cancel: () => void }\ninterface RunPoolOptions {\n  shouldPause?: () => Promise<void>\n  shouldCancel?: () => boolean\n}\n\nasync function runPool<T, K>(\n  poolLimit: number,\n  array: T[],\n  iteratorFn: IteratorFn<T, K>,\n  options: RunPoolOptions = {},\n) {\n  const results: Promise<{ ok: true; value: K } | { ok: false; error: Error }>[] = []\n  const activePromises = new Set<Promise<any>>()\n  const { shouldPause, shouldCancel } = options\n\n  for (const item of array) {\n    if (shouldCancel?.()) break\n\n    if (shouldPause) {\n      await shouldPause()\n    }\n\n    if (shouldCancel?.()) break\n\n    const promise = Promise.resolve()\n      .then(() => iteratorFn(item, array))\n      .then<{ ok: true; value: K }>((value) => ({ ok: true, value }))\n      .catch<{ ok: false; error: Error }>((error) => ({ ok: false, error }))\n\n    results.push(promise)\n\n    if (poolLimit < array.length) {\n      activePromises.add(promise)\n      const cleanup = () => activePromises.delete(promise)\n      promise.then(cleanup, cleanup)\n\n      if (activePromises.size >= poolLimit) {\n        await Promise.race(activePromises)\n      }\n    }\n  }\n\n  return await Promise.all(results)\n}\n\nexport const asyncPool = <T, K = any>(\n  poolLimit: number,\n  array: T[],\n  iteratorFn: IteratorFn<T, K>,\n) => {\n  return runPool(poolLimit, array, iteratorFn)\n}\n\nexport const createAsyncPool = <T, K>(\n  poolLimit: number,\n  array: T[],\n  iteratorFn: IteratorFn<T, K>,\n) => {\n  let paused = false\n  let cancelled = false\n  let resumeResolve: (() => void) | null = null\n\n  const controller: PoolController = {\n    pause() {\n      paused = true\n    },\n    resume() {\n      paused = false\n      resumeResolve?.()\n      resumeResolve = null\n    },\n    cancel() {\n      cancelled = true\n      resumeResolve?.()\n      resumeResolve = null\n    },\n  }\n\n  const shouldPause = async () => {\n    if (paused) {\n      await new Promise<void>((resolve) => (resumeResolve = resolve))\n    }\n  }\n\n  const shouldCancel = () => cancelled\n\n  const run = () => runPool(poolLimit, array, iteratorFn, { shouldPause, shouldCancel })\n\n  return { run, controller }\n}\n\nexport const getUserAgent = () => {\n  const appSettings = useAppSettingsStore()\n  return appSettings.app.userAgent || APP_TITLE + '/' + APP_VERSION\n}\n\nexport const getGitHubApiAuthorization = () => {\n  const appSettings = useAppSettingsStore()\n  return appSettings.app.githubApiToken ? `Bearer ${appSettings.app.githubApiToken}` : ''\n}\n\n// System ScheduledTask Helper\nexport const getTaskSchXmlString = async (delay = 30) => {\n  const { appPath } = useEnvStore().env\n\n  const xml = /*xml*/ `<?xml version=\"1.0\" encoding=\"UTF-16\"?>\n<Task version=\"1.2\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n  <RegistrationInfo>\n    <Description>${APP_TITLE} at startup</Description>\n    <URI>\\\\${APP_TITLE}</URI>\n  </RegistrationInfo>\n  <Triggers>\n    <LogonTrigger>\n      <Enabled>true</Enabled>\n      <Delay>PT${delay}S</Delay>\n    </LogonTrigger>\n  </Triggers>\n  <Principals>\n    <Principal id=\"Author\">\n      <LogonType>InteractiveToken</LogonType>\n      <RunLevel>HighestAvailable</RunLevel>\n    </Principal>\n  </Principals>\n  <Settings>\n    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>\n    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>\n    <AllowHardTerminate>true</AllowHardTerminate>\n    <StartWhenAvailable>false</StartWhenAvailable>\n    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>\n    <IdleSettings>\n      <StopOnIdleEnd>true</StopOnIdleEnd>\n      <RestartOnIdle>false</RestartOnIdle>\n    </IdleSettings>\n    <AllowStartOnDemand>true</AllowStartOnDemand>\n    <Enabled>true</Enabled>\n    <Hidden>false</Hidden>\n    <RunOnlyIfIdle>false</RunOnlyIfIdle>\n    <WakeToRun>false</WakeToRun>\n    <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>\n    <Priority>7</Priority>\n  </Settings>\n  <Actions Context=\"Author\">\n    <Exec>\n      <Command>${appPath}</Command>\n      <Arguments>tasksch</Arguments>\n    </Exec>\n  </Actions>\n</Task>\n`\n\n  return xml\n}\n\nexport const setIntervalImmediately = (func: () => void, interval: number) => {\n  func()\n  return setInterval(func, interval)\n}\n\nconst isPlainObject = (obj: any) => {\n  return typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object Object]'\n}\n\nexport const deepAssign = (...args: any[]) => {\n  const len = args.length\n  let target = args[0]\n  if (!isPlainObject(target)) {\n    target = {}\n  }\n  for (let i = 1; i < len; i++) {\n    const source = args[i]\n    if (isPlainObject(source)) {\n      for (const s in source) {\n        if (s === '__proto__' || target === source[s]) {\n          continue\n        }\n        if (isPlainObject(source[s])) {\n          target[s] = deepAssign(target[s], source[s])\n        } else {\n          target[s] = source[s]\n        }\n      }\n    }\n  }\n  return target\n}\n\nexport const readonly = <T>(obj: T): T => {\n  if (typeof obj !== 'object' || obj === null) return obj\n  return new Proxy(obj, {\n    get(target, key) {\n      const result = Reflect.get(target, key)\n      if (typeof result === 'object' && result !== null) {\n        return readonly(result)\n      }\n      return result\n    },\n    set(target, key) {\n      console.warn(`Set operation on key \"${String(key)}\" failed: target is readonly.`, target)\n      return true\n    },\n    deleteProperty(target, key) {\n      console.warn(`Delete operation on key \"${String(key)}\" failed: target is readonly.`, target)\n      return true\n    },\n    defineProperty(target, key) {\n      console.warn(\n        `DefineProperty operation on key \"${String(key)}\" failed: target is readonly.`,\n        target,\n      )\n      return false\n    },\n    setPrototypeOf(target) {\n      console.warn(`SetPrototypeOf operation failed: target is readonly.`, target)\n      return false\n    },\n  })\n}\n\nexport const normalizeBase64 = (str: string): string => {\n  const normalized = str.trim().replace(/\\s+/g, '').replace(/-/g, '+').replace(/_/g, '/')\n\n  const padding = (4 - (normalized.length % 4)) % 4\n  return normalized + '='.repeat(padding)\n}\n\nexport const base64UrlEncode = (str: string): string => {\n  return base64Encode(str).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nexport const base64Encode = (str: string): string => {\n  const bytes = new TextEncoder().encode(str)\n  const len = bytes.length\n  const chars = Array(len)\n\n  for (let i = 0; i < len; i++) {\n    chars[i] = String.fromCharCode(bytes[i]!)\n  }\n\n  return btoa(chars.join(''))\n}\n\nexport const base64Decode = (input: string): string => {\n  const base64 = normalizeBase64(input)\n  const binary = atob(base64)\n  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0))\n  return new TextDecoder().decode(bytes)\n}\n\nexport const stringifyNoFolding = (content: any) => {\n  // Disable string folding\n  return stringify(content, { lineWidth: 0, minContentWidth: 0 })\n}\n\nconst regexCache = new Map<string, RegExp>()\n\nexport const buildSmartRegExp = (pattern: string, flags = '') => {\n  const key = pattern + '::' + flags\n  if (regexCache.has(key)) return regexCache.get(key)!\n\n  let r\n  try {\n    r = new RegExp(pattern, flags)\n  } catch {\n    const escaped = pattern.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n    r = new RegExp(escaped, flags)\n  }\n\n  regexCache.set(key, r)\n  return r\n}\n"
  },
  {
    "path": "frontend/src/utils/restorer.ts",
    "content": "import * as Defaults from '@/constant/profile'\nimport {\n  Inbound,\n  Outbound,\n  RuleAction,\n  RulesetType,\n  RuleType as RouteRuleType,\n  DnsServer,\n} from '@/enums/kernel'\n\nimport { deepAssign, sampleID } from './others'\nimport { useProfilesStore, useRulesetsStore } from '@/stores'\n\nconst supportedRuleTypes = [\n  RouteRuleType.Inbound,\n  RouteRuleType.Network,\n  RouteRuleType.Protocol,\n  RouteRuleType.Domain,\n  RouteRuleType.DomainSuffix,\n  RouteRuleType.DomainKeyword,\n  RouteRuleType.DomainRegex,\n  RouteRuleType.SourceIPCidr,\n  RouteRuleType.IPCidr,\n  RouteRuleType.SourcePort,\n  RouteRuleType.SourcePortRange,\n  RouteRuleType.Port,\n  RouteRuleType.PortRange,\n  RouteRuleType.ProcessName,\n  RouteRuleType.ProcessPath,\n  RouteRuleType.ProcessPathRegex,\n  RouteRuleType.RuleSet,\n  RouteRuleType.IpIsPrivate,\n  RouteRuleType.ClashMode,\n]\n\nconst buildTagIdMapping = (prefix: string, arr?: Recordable[]): Recordable<string> => {\n  if (!arr) return {}\n  return arr.reduce((p, c, i) => ((p[c.tag] = prefix + i), p), {})\n}\n\ntype RestoreProfileOptions = {\n  extraOutboundsIds?: Recordable\n}\n\nexport const restoreProfile = (\n  config: Recordable,\n  name = sampleID(),\n  options: RestoreProfileOptions = {},\n) => {\n  const template = useProfilesStore().getProfileTemplate()\n\n  const { extraOutboundsIds } = options\n\n  const InboundsIds = buildTagIdMapping('in-', config.inbounds)\n  const OutboundsIds = buildTagIdMapping('out-', config.outbounds)\n  const RouteRuleSetIds = buildTagIdMapping('ruleset-', config.route?.rule_set)\n  const DnsServersIds = buildTagIdMapping('dns-', config.dns?.servers)\n\n  extraOutboundsIds && deepAssign(OutboundsIds, extraOutboundsIds)\n\n  const profile: IProfile = {\n    id: sampleID(),\n    name,\n    log: deepAssign(Defaults.DefaultLog(), config.log),\n    experimental: restoreExperimental(config.experimental, OutboundsIds),\n    inbounds: restoreInbounds(config.inbounds || [], InboundsIds),\n    outbounds: restoreOutbounds(config.outbounds || [], OutboundsIds),\n    route: {\n      rule_set: restoreRouteRuleset(config.route?.rule_set || [], RouteRuleSetIds, OutboundsIds),\n      rules: restoreRouteRules(\n        config.route?.rules || [],\n        InboundsIds,\n        OutboundsIds,\n        RouteRuleSetIds,\n        DnsServersIds,\n      ),\n      auto_detect_interface:\n        config.route?.auto_detect_interface ?? template.route.auto_detect_interface,\n      find_process: config.route?.find_process ?? template.route.find_process,\n      default_interface: config.route?.default_interface ?? template.route.default_interface,\n      final: OutboundsIds[config.route?.final] ?? template.route.final,\n      default_domain_resolver: {\n        server:\n          DnsServersIds[config.route?.default_domain_resolver?.server] ??\n          template.route.default_domain_resolver.server,\n        client_subnet:\n          config.route?.default_domain_resolver?.client_subnet ??\n          template.route.default_domain_resolver.client_subnet,\n      },\n    },\n    dns: {\n      disable_cache: config.dns?.disable_cache ?? template.dns.disable_cache,\n      disable_expire: config.dns?.disable_expire ?? template.dns.disable_expire,\n      independent_cache: config.dns?.independent_cache ?? template.dns.independent_cache,\n      final: DnsServersIds[config.dns?.final] ?? template.dns.final,\n      strategy: config.dns?.strategy ?? template.dns.strategy,\n      client_subnet: config.dns?.client_subnet ?? template.dns.client_subnet,\n      servers: restoreDnsServers(config.dns?.servers || [], DnsServersIds, OutboundsIds),\n      rules: restoreDnsRules(config.dns?.rules || [], InboundsIds, RouteRuleSetIds, DnsServersIds),\n    },\n    mixin: Defaults.DefaultMixin(),\n    script: Defaults.DefaultScript(),\n  }\n\n  return profile\n}\n\nconst restoreExperimental = (raw: Recordable, OutboundsIds: Recordable): IExperimental => {\n  const template = Defaults.DefaultExperimental()\n  const experimental = deepAssign(template, raw)\n  experimental.clash_api.external_ui_download_detour =\n    OutboundsIds[template.clash_api.external_ui_download_detour]\n  return experimental\n}\n\nconst restoreInbounds = (inbounds: Recordable[], InboundsIds: Recordable): IInbound[] => {\n  return inbounds.flatMap((raw) => {\n    if (![Inbound.Mixed, Inbound.Http, Inbound.Socks, Inbound.Tun].includes(raw.type)) return []\n    const inbound: IInbound = {\n      id: InboundsIds[raw.tag],\n      tag: raw.tag,\n      type: raw.type,\n      enable: true,\n    }\n    if (raw.type === Inbound.Tun) {\n      const template = Defaults.DefaultInboundTun()\n      inbound.tun = {\n        interface_name: raw.interface_name ?? template.interface_name,\n        address: raw.address ?? template.address,\n        mtu: raw.mtu ?? template.mtu,\n        auto_route: raw.auto_route ?? template.auto_route,\n        strict_route: raw.strict_route ?? template.strict_route,\n        route_address: raw.route_address ?? template.route_address,\n        route_exclude_address: raw.route_exclude_address ?? template.route_exclude_address,\n        endpoint_independent_nat: raw.endpoint_independent_nat ?? template.endpoint_independent_nat,\n        stack: raw.stack ?? template.stack,\n      }\n    }\n    if ([Inbound.Mixed, Inbound.Http, Inbound.Socks].includes(raw.type)) {\n      const template = Defaults.DefaultInboundMixed()\n      inbound[raw.type as Exclude<Inbound, Inbound.Tun>] = {\n        listen: {\n          listen: raw.listen ?? template.listen.listen,\n          listen_port: raw.listen_port ?? template.listen.listen_port,\n          tcp_fast_open: raw.tcp_fast_open ?? template.listen.tcp_fast_open,\n          tcp_multi_path: raw.tcp_multi_path ?? template.listen.tcp_multi_path,\n          udp_fragment: raw.udp_fragment ?? template.listen.udp_fragment,\n        },\n        users: raw.users?.map((user: any) => user.username + ':' + user.password) ?? template.users,\n      }\n    }\n    return inbound\n  })\n}\n\nconst restoreOutbounds = (outbounds: Recordable[], OutboundsIds: Recordable): IOutbound[] => {\n  return outbounds.flatMap((raw) => {\n    if (![Outbound.Selector, Outbound.Urltest].includes(raw.type)) {\n      return []\n    }\n    const outbound = Defaults.DefaultOutbound()\n    outbound.id = OutboundsIds[raw.tag]\n    outbound.tag = raw.tag\n    outbound.type = raw.type\n    if ([Outbound.Selector, Outbound.Urltest].includes(raw.type)) {\n      if ('interrupt_exist_connections' in raw) {\n        outbound.interrupt_exist_connections = raw.interrupt_exist_connections\n      }\n      outbound.outbounds = raw.outbounds?.flatMap((tag: string) => {\n        if (!OutboundsIds[tag]) return []\n        const isBuiltIn = [Outbound.Direct, Outbound.Block].includes(tag as Outbound)\n        return {\n          id: isBuiltIn ? tag : OutboundsIds[tag],\n          type: 'Built-in',\n          tag,\n        }\n      })\n    }\n    if (Outbound.Urltest === raw.type) {\n      if ('url' in raw) {\n        outbound.url = raw.url\n      }\n      if ('interval' in raw) {\n        outbound.interval = raw.interval\n      }\n      if ('tolerance' in raw) {\n        outbound.tolerance = raw.tolerance\n      }\n    }\n    return outbound\n  })\n}\n\nconst restoreRouteRuleset = (\n  rulesets: Recordable[],\n  RouteRuleSetIds: Recordable,\n  OutboundsIds: Recordable,\n): IRuleSet[] => {\n  const rulesetsStore = useRulesetsStore()\n  return rulesets.flatMap((raw) => {\n    const ruleset = Defaults.DefaultRouteRuleset()\n    ruleset.id = RouteRuleSetIds[raw.tag]\n    ruleset.type = raw.type\n    ruleset.tag = raw.tag\n\n    if (raw.type === RulesetType.Inline) {\n      if ('rules' in raw) {\n        ruleset.rules = JSON.stringify(raw.rules, null, 2)\n      }\n    } else if (raw.type === RulesetType.Local) {\n      if ('path' in raw) {\n        const r = rulesetsStore.rulesets.find((v) => v.path === raw.path.replace('../', 'data/'))\n        if (r) {\n          ruleset.path = r.id\n        } else {\n          ruleset.path = raw.path\n        }\n      }\n      if ('format' in raw) {\n        ruleset.format = raw.format\n      }\n    } else if (raw.type === RulesetType.Remote) {\n      if ('format' in raw) {\n        ruleset.format = raw.format\n      }\n      if ('url' in raw) {\n        ruleset.url = raw.url\n      }\n      if ('download_detour' in raw) {\n        ruleset.download_detour = OutboundsIds[raw.download_detour]\n      }\n      if ('update_interval' in raw) {\n        ruleset.update_interval = raw.update_interval\n      }\n    }\n    return ruleset\n  })\n}\n\nconst restoreRouteRules = (\n  rules: Recordable[],\n  InboundsIds: Recordable,\n  OutboundsIds: Recordable,\n  RouteRuleSetIds: Recordable,\n  DnsServersIds: Recordable,\n): IRule[] => {\n  return rules.flatMap((raw, i) => {\n    const rule = Defaults.DefaultRouteRule()\n\n    rule.id = 'rule-' + i\n    rule.action = raw.action || RuleAction.Route\n\n    const hits = supportedRuleTypes.filter((key) => key in raw)\n    if (hits.length === 1) {\n      rule.type = hits[0] as any\n    } else {\n      rule.type = RouteRuleType.Inline\n    }\n\n    if (rule.type === RouteRuleType.Inline) {\n      rule.payload = JSON.stringify(\n        {\n          ...raw,\n          action: undefined,\n          invert: undefined,\n          outbound: undefined,\n          sniffer: undefined,\n          strategy: undefined,\n          server: undefined,\n        },\n        null,\n        2,\n      )\n    } else if (rule.type === RouteRuleType.Inbound) {\n      rule.payload = InboundsIds[raw[rule.type]]\n    } else if (rule.type === RouteRuleType.RuleSet) {\n      rule.payload = raw[rule.type].map((tag: string) => RouteRuleSetIds[tag]).join(',')\n    } else {\n      rule.payload = String(raw[rule.type])\n    }\n\n    if (RuleAction.Route === raw.action) {\n      rule.outbound = OutboundsIds[raw.outbound]\n    } else if (RuleAction.Reject === raw.action) {\n      if ('method' in raw) {\n        rule.outbound = raw.method\n      }\n    } else if (RuleAction.RouteOptions === raw.action) {\n      rule.outbound = JSON.stringify(\n        {\n          ...raw,\n          action: undefined,\n          invert: undefined,\n          ...supportedRuleTypes.reduce((p, c) => ((p[c] = undefined), p), {} as Recordable),\n        },\n        null,\n        2,\n      )\n    } else if (RuleAction.Sniff === raw.action) {\n      if ('sniffer' in raw) {\n        rule.sniffer = Array.isArray(raw.sniffer) ? raw.sniffer : [raw.sniffer]\n      }\n    } else if (RuleAction.Resolve === raw.action) {\n      if ('strategy' in raw) {\n        rule.strategy = raw.strategy\n      }\n      if ('server' in raw) {\n        rule.server = DnsServersIds[raw.server]\n      }\n    }\n    if ('invert' in raw) {\n      rule.invert = raw.invert\n    }\n    return rule\n  })\n}\n\nconst restoreDnsServers = (\n  servers: Recordable[],\n  DnsServersIds: Recordable,\n  OutboundsIds: Recordable,\n): IDNSServer[] => {\n  return servers.flatMap((raw) => {\n    if (!raw.type) return []\n    const server = Defaults.DefaultDnsServer()\n    server.id = DnsServersIds[raw.tag]\n    server.tag = raw.tag\n    server.type = raw.type\n    if (\n      [\n        DnsServer.Local,\n        DnsServer.Tcp,\n        DnsServer.Udp,\n        DnsServer.Tls,\n        DnsServer.Quic,\n        DnsServer.Https,\n        DnsServer.H3,\n        DnsServer.Dhcp,\n      ].includes(raw.type)\n    ) {\n      if ('detour' in raw) {\n        server.detour = OutboundsIds[raw.detour]\n      }\n      if ('domain_resolver' in raw) {\n        server.domain_resolver = DnsServersIds[raw.domain_resolver]\n      }\n      if (\n        [\n          DnsServer.Tcp,\n          DnsServer.Udp,\n          DnsServer.Tls,\n          DnsServer.Quic,\n          DnsServer.Https,\n          DnsServer.H3,\n        ].includes(raw.type)\n      ) {\n        if ('server' in raw) {\n          server.server = raw.server\n        }\n        if ('server_port' in raw) {\n          server.server_port = raw.server_port\n        }\n        if ([DnsServer.Https, DnsServer.H3].includes(raw.type)) {\n          if ('path' in raw) {\n            server.path = raw.path\n          }\n        }\n      }\n    } else if (DnsServer.Hosts === server.type) {\n      if ('path' in raw) {\n        server.hosts_path = raw.path\n      }\n      if ('predefined' in raw) {\n        server.predefined = Object.entries<string[] | string>(raw.predefined).reduce(\n          (p, [key, value]) => {\n            p[key] = Array.isArray(value) ? value.join(',') : value\n            return p\n          },\n          {} as Recordable,\n        )\n      }\n    } else if (DnsServer.Dhcp === server.type) {\n      if ('interface' in raw) {\n        server.interface = raw.interface\n      }\n    } else if (DnsServer.FakeIP === server.type) {\n      if ('inet4_range' in raw) {\n        server.inet4_range = raw.inet4_range\n      }\n      if ('inet6_range' in raw) {\n        server.inet6_range = raw.inet6_range\n      }\n    }\n    return server\n  })\n}\n\nconst restoreDnsRules = (\n  rules: Recordable[],\n  InboundsIds: Recordable,\n  RouteRuleSetIds: Recordable,\n  DnsServersIds: Recordable,\n): IDNSRule[] => {\n  return rules.flatMap((raw: Recordable, i) => {\n    const rule = Defaults.DefaultDnsRule()\n    rule.id = 'rule-' + i\n    rule.action = raw.action || RuleAction.Route\n\n    const hits = supportedRuleTypes.filter((key) => key in raw)\n    if (hits.length === 1) {\n      rule.type = hits[0] as any\n    } else {\n      rule.type = RouteRuleType.Inline\n    }\n\n    if (rule.type === RouteRuleType.Inline) {\n      rule.payload = JSON.stringify(\n        {\n          ...raw,\n          action: undefined,\n          invert: undefined,\n          client_subnet: undefined,\n          disable_cache: undefined,\n          strategy: undefined,\n          server: undefined,\n        },\n        null,\n        2,\n      )\n    } else if (rule.type === RouteRuleType.Inbound) {\n      rule.payload = InboundsIds[raw[rule.type]]\n    } else if (rule.type === RouteRuleType.RuleSet) {\n      rule.payload = raw[rule.type].map((tag: string) => RouteRuleSetIds[tag]).join(',')\n    } else {\n      rule.payload = raw[rule.type]\n    }\n\n    if (RuleAction.Route === raw.action) {\n      if ('server' in raw) {\n        rule.server = DnsServersIds[raw.server]\n      }\n      if ('strategy' in raw) {\n        rule.strategy = raw.strategy\n      }\n    } else if (RuleAction.Reject === raw.action) {\n      if ('method' in raw) {\n        rule.server = raw.method\n      }\n    } else if ([RuleAction.RouteOptions, RuleAction.Predefined].includes(raw.action)) {\n      rule.server = JSON.stringify(\n        {\n          ...raw,\n          action: undefined,\n          invert: undefined,\n          disable_cache: undefined,\n          ...supportedRuleTypes.reduce((p, c) => ((p[c] = undefined), p), {} as Recordable),\n        },\n        null,\n        2,\n      )\n    }\n    if ([RuleAction.Route, RuleAction.RouteOptions].includes(raw.action)) {\n      if ('disable_cache' in raw) {\n        rule.disable_cache = raw.disable_cache\n      }\n      if ('client_subnet' in raw) {\n        rule.client_subnet = raw.client_subnet\n      }\n    }\n    if ('invert' in raw) {\n      rule.invert = raw.invert\n    }\n    return rule\n  })\n}\n"
  },
  {
    "path": "frontend/src/utils/tray.ts",
    "content": "import {\n  Notify,\n  RestartApp,\n  EventsOn,\n  EventsOff,\n  ShowMainWindow,\n  UpdateTrayAndMenus,\n} from '@/bridge'\nimport { ColorOptions, ThemeOptions } from '@/constant/app'\nimport { ModeOptions } from '@/constant/kernel'\nimport i18n from '@/lang'\nimport {\n  useAppSettingsStore,\n  useKernelApiStore,\n  useEnvStore,\n  usePluginsStore,\n  useAppStore,\n} from '@/stores'\nimport {\n  debounce,\n  exitApp,\n  handleChangeMode,\n  APP_TITLE,\n  APP_VERSION,\n  handleUseProxy,\n} from '@/utils'\n\nimport type { MenuItem } from '@/types/app'\n\nconst getTrayIcons = () => {\n  const envStore = useEnvStore()\n  const appSettings = useAppSettingsStore()\n  const kernelApiStore = useKernelApiStore()\n\n  const themeMode = appSettings.themeMode\n  const ext = envStore.env.os === 'linux' ? '.png' : '.ico'\n  const folder = envStore.env.os === 'linux' ? 'imgs' : 'icons'\n  let icon = `data/.cache/${folder}/tray_normal_${themeMode}${ext}`\n\n  if (kernelApiStore.running) {\n    if (kernelApiStore.config.tun.enable) {\n      icon = `data/.cache/${folder}/tray_tun_${themeMode}${ext}`\n    } else if (envStore.systemProxy) {\n      icon = `data/.cache/${folder}/tray_proxy_${themeMode}${ext}`\n    }\n  }\n  return icon\n}\n\nconst generateUniqueEventsForMenu = (menus: MenuItem[]) => {\n  const { t } = i18n.global\n  const MenuItemHandlerMap: Recordable<() => void> = {}\n\n  EventsOff('onMenuItemClick')\n  EventsOn('onMenuItemClick', (id) => MenuItemHandlerMap[id]?.())\n\n  let index = 0\n  function processMenu(menu: MenuItem) {\n    const _menu = { ...menu, text: t(menu.text || ''), tooltip: t(menu.tooltip || '') }\n    const { event, children } = menu\n\n    if (event) {\n      _menu.event = index + '_' + menu.text\n      MenuItemHandlerMap[_menu.event] = event as any\n    }\n\n    if (children && children.length > 0) {\n      _menu.children = children.map(processMenu)\n    }\n\n    index += 1\n    return _menu\n  }\n\n  return menus.map(processMenu)\n}\n\nconst getTrayMenus = () => {\n  const appStore = useAppStore()\n  const envStore = useEnvStore()\n  const appSettings = useAppSettingsStore()\n  const kernelApiStore = useKernelApiStore()\n  const pluginsStore = usePluginsStore()\n\n  let pluginMenus: MenuItem[] = []\n  let pluginMenusHidden = !appSettings.app.addPluginToMenu\n\n  let groupMenus: MenuItem[] = []\n  const groupMenusHidden = !appSettings.app.addGroupToMenu\n\n  if (!groupMenusHidden) {\n    const { proxies } = kernelApiStore\n    if (!proxies) return []\n    groupMenus = Object.values(proxies)\n      .filter((v) => ['Selector', 'URLTest'].includes(v.type) && v.name !== 'GLOBAL')\n      .concat(proxies.GLOBAL || [])\n      .map((group) => {\n        const all = (group.all || [])\n          .filter((proxy) => {\n            const history = proxies[proxy]?.history || []\n            const alive = (history[history.length - 1]?.delay || 0) > 0\n            return (\n              appSettings.app.kernel.unAvailable ||\n              ['direct', 'block'].includes(proxy) ||\n              proxies[proxy]?.all ||\n              alive\n            )\n          })\n          .map((proxy) => {\n            const history = proxies[proxy]?.history || []\n            const delay = history[history.length - 1]?.delay || 0\n            return { ...proxies[proxy], delay }\n          })\n          .sort((a, b) => {\n            if (!appSettings.app.kernel.sortByDelay || a.delay === b.delay) return 0\n            if (!a.delay) return 1\n            if (!b.delay) return -1\n            return a.delay - b.delay\n          })\n        return { ...group, all }\n      })\n      .map((group) => {\n        return {\n          type: 'item',\n          text: group.name,\n          show: true,\n          children: group.all.map((proxy) => {\n            return {\n              type: 'item',\n              text: proxy.name,\n              show: true,\n              checked: proxy.name === group.now,\n              event: () => {\n                handleUseProxy(group, proxy)\n              },\n            }\n          }),\n        }\n      })\n  }\n\n  if (!pluginMenusHidden) {\n    const filtered = pluginsStore.plugins.filter(\n      (plugin) => Object.keys(plugin.menus).length && !plugin.disabled,\n    )\n    pluginMenusHidden = filtered.length === 0\n    pluginMenus = filtered.map(({ id, name, menus }) => {\n      return {\n        type: 'item',\n        text: name,\n        children: Object.entries(menus).map(([text, event]) => {\n          return {\n            type: 'item',\n            text,\n            event: () => {\n              pluginsStore.manualTrigger(id, event as any).catch((err: any) => {\n                Notify('Error', err.message || err)\n              })\n            },\n          }\n        }),\n      }\n    })\n  }\n\n  const trayMenus: MenuItem[] = [\n    {\n      type: 'item',\n      text: 'tray.showMainWindow',\n      hidden: envStore.env.os === 'windows',\n      event: ShowMainWindow,\n    },\n    {\n      type: 'separator',\n      hidden: envStore.env.os === 'windows',\n    },\n    {\n      type: 'item',\n      text: 'kernel.mode',\n      hidden: !kernelApiStore.running,\n      children: ModeOptions.map((mode) => ({\n        type: 'item',\n        text: mode.label,\n        checked: kernelApiStore.config.mode === mode.value,\n        event: () => handleChangeMode(mode.value),\n      })),\n    },\n    {\n      type: 'item',\n      text: 'tray.proxyGroup',\n      hidden: groupMenusHidden || !kernelApiStore.running,\n      children: groupMenus,\n    },\n    {\n      type: 'item',\n      text: 'tray.kernel',\n      children: [\n        {\n          type: 'item',\n          text: 'tray.startKernel',\n          hidden: kernelApiStore.running,\n          event: kernelApiStore.startCore,\n        },\n        {\n          type: 'item',\n          text: 'tray.restartKernel',\n          hidden: !kernelApiStore.running,\n          event: kernelApiStore.restartCore,\n        },\n        {\n          type: 'item',\n          text: 'tray.stopKernel',\n          hidden: !kernelApiStore.running,\n          event: kernelApiStore.stopCore,\n        },\n      ],\n    },\n    {\n      type: 'separator',\n      hidden: !kernelApiStore.running,\n    },\n    {\n      type: 'item',\n      text: 'tray.proxy',\n      hidden: !kernelApiStore.running,\n      children: [\n        {\n          type: 'item',\n          text: 'tray.setSystemProxy',\n          hidden: envStore.systemProxy,\n          event: envStore.setSystemProxy,\n        },\n        {\n          type: 'item',\n          text: 'tray.clearSystemProxy',\n          hidden: !envStore.systemProxy,\n          event: envStore.clearSystemProxy,\n        },\n      ],\n    },\n    {\n      type: 'item',\n      text: 'tray.tun',\n      hidden: !kernelApiStore.running,\n      children: [\n        {\n          type: 'item',\n          text: 'tray.enableTunMode',\n          hidden: kernelApiStore.config.tun.enable,\n          event: () => kernelApiStore.updateConfig('tun', { enable: true }),\n        },\n        {\n          type: 'item',\n          text: 'tray.disableTunMode',\n          hidden: !kernelApiStore.config.tun.enable,\n          event: () => kernelApiStore.updateConfig('tun', { enable: false }),\n        },\n      ],\n    },\n    {\n      type: 'item',\n      text: 'settings.general',\n      children: [\n        {\n          type: 'item',\n          text: 'settings.theme.name',\n          children: ThemeOptions.map((theme) => ({\n            type: 'item',\n            text: theme.label,\n            checked: appSettings.app.theme === theme.value,\n            event: () => (appSettings.app.theme = theme.value),\n          })),\n        },\n        {\n          type: 'item',\n          text: 'settings.color.name',\n          children: ColorOptions.map((color) => ({\n            type: 'item',\n            text: color.label,\n            checked: appSettings.app.color === color.value,\n            event: () => (appSettings.app.color = color.value),\n          })),\n        },\n        {\n          type: 'item',\n          text: 'settings.lang.name',\n          children: appStore.locales.map((v) => ({\n            type: 'item',\n            text: v.label,\n            checked: appSettings.app.lang === v.value,\n            event: () => (appSettings.app.lang = v.value),\n          })),\n        },\n      ],\n    },\n    {\n      type: 'item',\n      text: 'tray.plugins',\n      hidden: pluginMenusHidden,\n      children: pluginMenus,\n    },\n    {\n      type: 'separator',\n    },\n    {\n      type: 'item',\n      text: 'tray.restart',\n      tooltip: 'tray.restartTip',\n      event: RestartApp,\n    },\n    {\n      type: 'item',\n      text: 'tray.exit',\n      tooltip: 'tray.exitTip',\n      event: exitApp,\n    },\n  ]\n\n  return trayMenus\n}\n\nexport const updateTrayAndMenus = debounce(async () => {\n  const trayMenus = getTrayMenus()\n  const trayIcons = getTrayIcons()\n  const pluginsStore = usePluginsStore()\n\n  const isDarwin = useEnvStore().env.os === 'darwin'\n  const title = isDarwin ? '' : APP_TITLE\n\n  const tray = { icon: trayIcons, title, tooltip: APP_TITLE + ' ' + APP_VERSION }\n\n  const [finalTray, finalMenus] = await pluginsStore.onTrayUpdateTrigger(tray, trayMenus)\n\n  await UpdateTrayAndMenus(finalTray, generateUniqueEventsForMenu(finalMenus) as any)\n}, 500)\n"
  },
  {
    "path": "frontend/src/views/HomeView/components/CommonController.vue",
    "content": "<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n'\n\nimport { TunStackOptions } from '@/constant/kernel'\nimport { useKernelApiStore } from '@/stores'\nimport { message } from '@/utils'\n\nconst { t } = useI18n()\nconst kernelApiStore = useKernelApiStore()\n\nconst createValueWatcher = (\n  initialValue: number | string | boolean,\n  callback: (value: number | string | boolean) => Promise<void>,\n) => {\n  let lastValue = initialValue\n  return (newValue: number | boolean) => {\n    if (newValue !== lastValue) {\n      lastValue = newValue\n      callback(newValue).catch((e) => message.error(e.message || e))\n    }\n  }\n}\n\nconst onPortSubmit = createValueWatcher(kernelApiStore.config.port, (port) =>\n  kernelApiStore.updateConfig('http', port),\n)\nconst onSocksPortSubmit = createValueWatcher(kernelApiStore.config['socks-port'], (port) =>\n  kernelApiStore.updateConfig('socks', port),\n)\nconst onMixedPortSubmit = createValueWatcher(kernelApiStore.config['mixed-port'], (port) =>\n  kernelApiStore.updateConfig('mixed', port),\n)\nconst onAllowLanChange = createValueWatcher(kernelApiStore.config['allow-lan'], (allow) =>\n  kernelApiStore.updateConfig('allow-lan', allow),\n)\nconst conStackChange = createValueWatcher(kernelApiStore.config.tun.stack, (stack) =>\n  kernelApiStore.updateConfig('tun-stack', { stack }),\n)\nconst onTunDeviceSubmit = createValueWatcher(kernelApiStore.config.tun.device, (device) =>\n  kernelApiStore.updateConfig('tun-device', { device }),\n)\nconst onInterfaceChange = createValueWatcher(\n  kernelApiStore.config['interface-name'],\n  (interface_name) => kernelApiStore.updateConfig('interface-name', { interface_name }),\n)\n</script>\n\n<template>\n  <div>\n    <Divider class=\"w-full mb-8\"> {{ t('home.overview.settingsTips') }} </Divider>\n    <div class=\"grid grid-cols-4 gap-8 pb-16\">\n      <Card :title=\"t('kernel.inbounds.mixedPort')\">\n        <Input\n          v-model=\"kernelApiStore.config['mixed-port']\"\n          :min=\"0\"\n          :max=\"65535\"\n          type=\"number\"\n          :border=\"false\"\n          editable\n          auto-size\n          class=\"w-full\"\n          @submit=\"onMixedPortSubmit\"\n        />\n      </Card>\n      <Card :title=\"t('kernel.inbounds.httpPort')\">\n        <Input\n          v-model=\"kernelApiStore.config.port\"\n          :min=\"0\"\n          :max=\"65535\"\n          type=\"number\"\n          :border=\"false\"\n          editable\n          auto-size\n          class=\"w-full\"\n          @submit=\"onPortSubmit\"\n        />\n      </Card>\n      <Card :title=\"t('kernel.inbounds.socksPort')\">\n        <Input\n          v-model=\"kernelApiStore.config['socks-port']\"\n          :min=\"0\"\n          :max=\"65535\"\n          type=\"number\"\n          editable\n          :border=\"false\"\n          auto-size\n          class=\"w-full\"\n          @submit=\"onSocksPortSubmit\"\n        />\n      </Card>\n      <Card :title=\"t('kernel.allow-lan')\">\n        <Switch v-model=\"kernelApiStore.config['allow-lan']\" @change=\"onAllowLanChange\" />\n      </Card>\n      <Card :title=\"t('kernel.inbounds.tun.stack')\">\n        <Select\n          v-model=\"kernelApiStore.config.tun.stack\"\n          :options=\"TunStackOptions\"\n          :border=\"false\"\n          auto-size\n          @change=\"conStackChange\"\n        />\n      </Card>\n      <Card :title=\"t('kernel.inbounds.tun.interface_name')\">\n        <Input\n          v-model=\"kernelApiStore.config.tun.device\"\n          editable\n          :border=\"false\"\n          auto-size\n          class=\"w-full\"\n          @submit=\"onTunDeviceSubmit\"\n        />\n      </Card>\n      <Card :title=\"t('kernel.route.default_interface')\">\n        <InterfaceSelect\n          v-model=\"kernelApiStore.config['interface-name']\"\n          :border=\"false\"\n          auto-size\n          @change=\"onInterfaceChange\"\n        />\n      </Card>\n      <Card :title=\"t('common.none')\"> </Card>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/HomeView/components/ConnectionsController.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, computed, onUnmounted } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { deleteConnection } from '@/api/kernel'\nimport { DraggableOptions } from '@/constant/app'\nimport { DefaultConnections } from '@/constant/kernel'\nimport { useBool } from '@/hooks'\nimport { useAppSettingsStore, useKernelApiStore } from '@/stores'\nimport { addToRuleSet, formatBytes, formatRelativeTime, message, picker } from '@/utils'\n\nimport type { PickerItem } from '@/components/Picker/index.vue'\nimport type { Column } from '@/components/Table/index.vue'\nimport type { Menu } from '@/types/app'\nimport type { CoreApiConnectionsData } from '@/types/kernel'\n\ntype TrafficCacheType = { up: number; down: number }\nconst TrafficCache: Record<string, TrafficCacheType> = {}\n\nconst appSettingsStore = useAppSettingsStore()\n\nconst columns = computed(() =>\n  (\n    [\n      {\n        title: 'home.connections.type',\n        align: 'center',\n        key: 'metadata.type',\n        hidden: !appSettingsStore.app.connections.visibility['metadata.type'],\n        sort: (a, b) => b.metadata.type.localeCompare(a.metadata.type),\n        customRender: ({ value, record }) => {\n          return value + '(' + record.metadata.network + ')'\n        },\n      },\n      {\n        title: 'home.connections.processPath',\n        key: 'metadata.processPath',\n        hidden: !appSettingsStore.app.connections.visibility['metadata.processPath'],\n        sort: (a, b) => b.metadata.processPath.localeCompare(a.metadata.processPath),\n      },\n      {\n        title: 'home.connections.host',\n        key: 'metadata.host',\n        hidden: !appSettingsStore.app.connections.visibility['metadata.host'],\n        sort: (a, b) => b.metadata.host.localeCompare(a.metadata.host),\n        customRender: ({ value, record }) => {\n          return value || record.metadata.destinationIP\n        },\n      },\n      {\n        title: 'home.connections.sourceIP',\n        align: 'center',\n        key: 'metadata.sourceIP',\n        hidden: !appSettingsStore.app.connections.visibility['metadata.sourceIP'],\n        sort: (a, b) => b.metadata.sourceIP.localeCompare(a.metadata.sourceIP),\n        customRender: ({ value, record }) => {\n          return value + ':' + record.metadata.sourcePort\n        },\n      },\n      {\n        title: 'home.connections.destinationIP',\n        align: 'center',\n        key: 'metadata.destinationIP',\n        hidden: !appSettingsStore.app.connections.visibility['metadata.destinationIP'],\n        sort: (a, b) => b.metadata.destinationIP.localeCompare(a.metadata.destinationIP),\n        customRender: ({ value, record }) => {\n          return value + ':' + record.metadata.destinationPort\n        },\n      },\n      {\n        title: 'home.connections.rule',\n        align: 'center',\n        key: 'rule',\n        hidden: !appSettingsStore.app.connections.visibility['rule'],\n        sort: (a, b) => b.rule.localeCompare(a.rule),\n        customRender: ({ value, record }) => {\n          return value + (record.rulePayload ? '::' + record.rulePayload : '')\n        },\n      },\n      {\n        title: 'home.connections.chains',\n        key: 'chains',\n        hidden: !appSettingsStore.app.connections.visibility['chains'],\n        sort: (a, b) => b.chains[0].localeCompare(a.chains[0]),\n        customRender: ({ value }) => value.slice().reverse().join(' :: '),\n      },\n      {\n        title: 'home.connections.uploadSpeed',\n        align: 'center',\n        key: 'up',\n        minWidth: '90px',\n        hidden: !appSettingsStore.app.connections.visibility['up'],\n        sort: (a, b) => b.upload - b.up - (a.upload - a.up),\n        customRender: ({ value, record }) => formatBytes(record.upload - value) + '/s',\n      },\n      {\n        title: 'home.connections.downSpeed',\n        align: 'center',\n        key: 'down',\n        minWidth: '90px',\n        hidden: !appSettingsStore.app.connections.visibility['down'],\n        sort: (a, b) => b.download - b.down - (a.download - a.down),\n        customRender: ({ value, record }) => formatBytes(record.download - value) + '/s',\n      },\n      {\n        title: 'home.connections.upload',\n        align: 'center',\n        key: 'upload',\n        hidden: !appSettingsStore.app.connections.visibility['upload'],\n        sort: (a, b) => b.upload - a.upload,\n        customRender: ({ value }) => formatBytes(value),\n      },\n      {\n        title: 'home.connections.download',\n        align: 'center',\n        key: 'download',\n        hidden: !appSettingsStore.app.connections.visibility['download'],\n        sort: (a, b) => b.download - a.download,\n        customRender: ({ value }) => formatBytes(value),\n      },\n      {\n        title: 'home.connections.time',\n        align: 'center',\n        key: 'start',\n        hidden: !appSettingsStore.app.connections.visibility['start'],\n        sort: (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime(),\n        customRender: ({ value }) => formatRelativeTime(value),\n      },\n    ] as Column[]\n  ).sort(\n    (a, b) =>\n      appSettingsStore.app.connections.order.indexOf(a.key) -\n      appSettingsStore.app.connections.order.indexOf(b.key),\n  ),\n)\n\nconst columnTitleMap = computed(() => {\n  const map: Record<string, string | undefined> = {}\n  appSettingsStore.app.connections.order.forEach(\n    (field) => (map[field] = columns.value.find((column) => column.key === field)?.title),\n  )\n  return map\n})\n\nconst menu: Menu[] = [\n  {\n    label: 'common.details',\n    handler: (record: Record<string, any>) => {\n      details.value = JSON.stringify(record, null, 2)\n      toggleDetails()\n    },\n  },\n  {\n    label: 'home.connections.close',\n    handler: async (record: Record<string, any>) => {\n      if (!isActive.value) return\n      try {\n        await deleteConnection(record.id)\n      } catch (error: any) {\n        console.log(error)\n        message.error(error)\n      }\n    },\n  },\n  ...(\n    [\n      ['home.connections.addToDirect', 'direct'],\n      ['home.connections.addToProxy', 'proxy'],\n      ['home.connections.addToReject', 'reject'],\n    ] as const\n  ).map(([label, ruleset]) => {\n    return {\n      label,\n      handler: async (record: Record<string, any>) => {\n        const options: PickerItem<Record<string, any>[]>[] = []\n        if (record.metadata.host) {\n          options.push({\n            label: t('kernel.rules.type.domain'),\n            value: { domain: record.metadata.host } as any,\n            description: record.metadata.host,\n          })\n          const domain_suffix = '.' + record.metadata.host.split('.').slice(1).join('.')\n          options.push({\n            label: t('kernel.rules.type.domain_suffix'),\n            value: {\n              domain_suffix: domain_suffix,\n            } as any,\n            description: domain_suffix,\n          })\n        }\n        if (record.metadata.destinationIP) {\n          options.push({\n            label: t('kernel.rules.type.ip_cidr'),\n            value: { ip_cidr: record.metadata.destinationIP + '/32' } as any,\n            description: record.metadata.destinationIP,\n          })\n        }\n        if (record.metadata.processPath) {\n          options.push({\n            label: t('kernel.rules.type.process_path'),\n            value: { process_path: record.metadata.processPath } as any,\n            description: record.metadata.processPath,\n          })\n        }\n        const payloads = await picker.multi('rulesets.selectRuleType', options)\n        try {\n          await addToRuleSet(ruleset, payloads)\n          message.success('common.success')\n        } catch (error: any) {\n          message.error(error)\n          console.log(error)\n        }\n      },\n    }\n  }),\n]\n\nconst details = ref()\nconst isActive = ref(true)\nconst keywords = ref('')\nconst dataSource = ref<(CoreApiConnectionsData['connections'][0] & TrafficCacheType)[]>([])\nconst disconnectedData = ref<CoreApiConnectionsData['connections']>([])\nconst [showDetails, toggleDetails] = useBool(false)\nconst [showSettings, toggleSettings] = useBool(false)\nconst [isPause, togglePause] = useBool(false)\nconst { t } = useI18n()\nconst kernelApiStore = useKernelApiStore()\n\nconst filteredConnections = computed(() => {\n  if (!keywords.value) return isActive.value ? dataSource.value : disconnectedData.value\n  return (isActive.value ? dataSource.value : disconnectedData.value).filter((connection) =>\n    Object.values(connection.metadata).some((v) =>\n      String(v).toLocaleLowerCase().includes(keywords.value.toLocaleLowerCase()),\n    ),\n  )\n})\n\nconst handleCloseAll = async () => {\n  try {\n    await Promise.all(\n      filteredConnections.value.map((connection) => deleteConnection(connection.id)),\n    )\n    disconnectedData.value.push(...filteredConnections.value)\n    dataSource.value = dataSource.value.filter(\n      (connection) => !filteredConnections.value.find((c) => c.id === connection.id),\n    )\n  } catch (error: any) {\n    message.error(error.message || error)\n  }\n}\n\nconst handleClearClosedConns = () => {\n  disconnectedData.value.splice(0)\n}\n\nconst handleResetConnections = () => {\n  appSettingsStore.app.connections = DefaultConnections()\n  message.success('common.success')\n}\n\nconst unregisterConnectionsHandler = kernelApiStore.onConnections((data) => {\n  if (isPause.value) return\n  const connections = data.connections || []\n\n  dataSource.value.forEach((connection) => {\n    // Record Disconnected Connections\n    const exist = connections.some((v) => v.id === connection.id)\n    !exist && disconnectedData.value.push(connection)\n  })\n\n  dataSource.value = connections.map((connection) => {\n    // Record Previous Traffic Information\n    const result = { ...connection, up: 0, down: 0 }\n    const cache = TrafficCache[connection.id]\n    result.up = cache?.up || connection.upload\n    result.down = cache?.down || connection.download\n    TrafficCache[connection.id] = {\n      down: connection.download,\n      up: connection.upload,\n    }\n    return result\n  })\n})\n\nonUnmounted(() => {\n  unregisterConnectionsHandler()\n})\n</script>\n\n<template>\n  <div class=\"flex flex-col h-full\">\n    <div class=\"flex items-center\">\n      <Radio\n        v-model=\"isActive\"\n        :options=\"[\n          { label: 'home.connections.active', value: true },\n          { label: 'home.connections.closed', value: false },\n        ]\"\n        size=\"small\"\n      />\n      <Input v-model=\"keywords\" clearable size=\"small\" placeholder=\"Search\" class=\"ml-8 flex-1\" />\n      <Button\n        :icon=\"isPause ? 'play' : 'pause'\"\n        size=\"small\"\n        type=\"text\"\n        class=\"ml-8\"\n        @click=\"togglePause\"\n      />\n      <Button\n        v-if=\"isActive\"\n        v-tips=\"'home.connections.closeAll'\"\n        icon=\"close\"\n        size=\"small\"\n        type=\"text\"\n        @click=\"handleCloseAll\"\n      />\n      <Button\n        v-else\n        v-tips=\"'common.clear'\"\n        icon=\"clear\"\n        size=\"small\"\n        type=\"text\"\n        @click=\"handleClearClosedConns\"\n      />\n      <Button icon=\"settings\" size=\"small\" type=\"text\" @click=\"toggleSettings\" />\n    </div>\n    <Table\n      class=\"flex-1 mt-8\"\n      :columns=\"columns\"\n      :menu=\"menu\"\n      :data-source=\"filteredConnections\"\n      sort=\"start\"\n    />\n  </div>\n\n  <Modal\n    v-model:open=\"showDetails\"\n    :submit=\"false\"\n    cancel-text=\"common.close\"\n    title=\"home.connections.details\"\n    max-height=\"80\"\n    max-width=\"80\"\n    mask-closable\n  >\n    <CodeViewer v-model=\"details\" />\n  </Modal>\n\n  <Modal\n    v-model:open=\"showSettings\"\n    :submit=\"false\"\n    mask-closable\n    max-height=\"80\"\n    cancel-text=\"common.close\"\n    title=\"home.connections.sort\"\n  >\n    <template #action>\n      <Button type=\"text\" class=\"mr-auto\" @click=\"handleResetConnections\">\n        {{ t('common.reset') }}\n      </Button>\n    </template>\n    <div v-draggable=\"[appSettingsStore.app.connections.order, DraggableOptions]\">\n      <Card v-for=\"column in appSettingsStore.app.connections.order\" :key=\"column\" class=\"mb-2\">\n        <div class=\"flex items-center justify-between py-2\">\n          <span class=\"font-bold\">{{ t(columnTitleMap[column] || column) }}</span>\n          <Switch v-model=\"appSettingsStore.app.connections.visibility[column]\" />\n        </div>\n      </Card>\n    </div>\n  </Modal>\n</template>\n"
  },
  {
    "path": "frontend/src/views/HomeView/components/GroupsController.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, computed, onActivated } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { getProxyDelay } from '@/api/kernel'\nimport {\n  ControllerCloseModeOptions,\n  DefaultCardColumns,\n  DefaultConcurrencyLimit,\n  DefaultControllerSensitivity,\n  DefaultTestTimeout,\n  DefaultTestURL,\n} from '@/constant/app'\nimport { ControllerCloseMode } from '@/enums/app'\nimport { useBool } from '@/hooks'\nimport { useAppSettingsStore, useKernelApiStore } from '@/stores'\nimport {\n  ignoredError,\n  sleep,\n  handleUseProxy,\n  message,\n  createAsyncPool,\n  buildSmartRegExp,\n} from '@/utils'\n\nconst expandedSet = ref<Set<string>>(new Set())\nconst loadingSet = ref<Set<string>>(new Set())\nconst filterKeywordsMap = ref<Record<string, string>>({})\n\nconst loading = ref(false)\n\nconst { t } = useI18n()\nconst [showMoreSettings, toggleMoreSettings] = useBool(false)\nconst appSettings = useAppSettingsStore()\nconst kernelApiStore = useKernelApiStore()\n\nconst groups = computed(() => {\n  const { proxies } = kernelApiStore\n  return Object.values(proxies)\n    .filter((v) => ['Selector', 'URLTest'].includes(v.type) && v.name !== 'GLOBAL')\n    .concat(proxies.GLOBAL || [])\n    .map((group) => {\n      const all = (group.all || [])\n        .filter((proxy) => {\n          const history = proxies[proxy]?.history || []\n          const alive = (history[history.length - 1]?.delay ?? 0) > 0\n          const condition1 =\n            appSettings.app.kernel.unAvailable ||\n            ['direct', 'block'].includes(proxy) ||\n            proxies[proxy]?.all ||\n            alive\n          const keywords = filterKeywordsMap.value[group.name]\n          const condition2 = keywords ? buildSmartRegExp(keywords, 'i').test(proxy) : true\n          return condition1 && condition2\n        })\n        .map((proxy) => {\n          const history = proxies[proxy]?.history || []\n          const delay = history[history.length - 1]?.delay || 0\n          return { ...proxies[proxy]!, delay }\n        })\n        .sort((a, b) => {\n          if (!appSettings.app.kernel.sortByDelay || a.delay === b.delay) return 0\n          if (!a.delay) return 1\n          if (!b.delay) return -1\n          return a.delay - b.delay\n        })\n\n      const chains = [group.now]\n      let tmp = proxies[group.now]\n      while (tmp) {\n        tmp.now && chains.push(tmp.now)\n        tmp = proxies[tmp.now]\n      }\n      return { ...group, all, chains }\n    })\n})\n\nconst useProxyWithCatchError = (group: any, proxy: any) => {\n  handleUseProxy(group, proxy).catch((err: any) => message.error(err.message || err))\n}\n\nconst toggleExpanded = (group: string) => {\n  if (expandedSet.value.has(group)) {\n    expandedSet.value.delete(group)\n  } else {\n    expandedSet.value.add(group)\n  }\n}\n\nconst expandAll = () => groups.value.forEach(({ name }) => expandedSet.value.add(name))\n\nconst collapseAll = () => expandedSet.value.clear()\n\nconst isExpanded = (group: string) => expandedSet.value.has(group)\n\nconst isLoading = (group: string) => loadingSet.value.has(group)\n\nconst isFiltered = (group: string) => filterKeywordsMap.value[group]\n\nconst handleGroupDelay = async (group: string) => {\n  const _group = kernelApiStore.proxies[group]\n  if (_group) {\n    let index = 0\n    let success = 0\n    let failure = 0\n\n    const delayTest = async (proxy: string) => {\n      index += 1\n      update(`Testing... ${index} / ${_group.all.length}, success: ${success} failure: ${failure}`)\n      const _proxy = kernelApiStore.proxies[proxy]\n      try {\n        loadingSet.value.add(proxy)\n        const { delay = 0 } = await getProxyDelay(\n          encodeURIComponent(proxy),\n          appSettings.app.kernel.testUrl || DefaultTestURL,\n          appSettings.app.kernel.testTimeout || DefaultTestTimeout,\n        )\n        success += 1\n        _proxy && _proxy.history.push({ delay })\n      } catch {\n        failure += 1\n        _proxy && _proxy.history.push({ delay: 0 })\n      }\n      update(`Testing... ${index} / ${_group.all.length}, success: ${success} failure: ${failure}`)\n      loadingSet.value.delete(proxy)\n    }\n\n    loadingSet.value.add(group)\n    const { run, controller } = createAsyncPool(\n      appSettings.app.kernel.concurrencyLimit || DefaultConcurrencyLimit,\n      _group.all,\n      delayTest,\n    )\n    const {\n      update,\n      destroy,\n      success: msgSuccess,\n    } = message.info('Testing...', 99999, () => {\n      controller.cancel()\n      message.warn('common.canceled')\n    })\n    await run()\n    loadingSet.value.delete(group)\n    msgSuccess(\n      `Completed. ${index} / ${_group.all.length}, success: ${success} failure: ${failure}`,\n    )\n    await sleep(3000)\n    destroy()\n  }\n}\n\nconst handleProxyDelay = async (proxy: string) => {\n  loadingSet.value.add(proxy)\n  try {\n    const { delay = 0 } = await getProxyDelay(\n      encodeURIComponent(proxy),\n      appSettings.app.kernel.testUrl || DefaultTestURL,\n      appSettings.app.kernel.testTimeout || DefaultTestTimeout,\n    )\n    const _proxy = kernelApiStore.proxies[proxy]\n    _proxy && _proxy.history.push({ delay })\n  } catch (error: any) {\n    message.error(error + ': ' + proxy)\n  }\n  loadingSet.value.delete(proxy)\n}\n\nconst handleRefresh = async () => {\n  loading.value = true\n  await ignoredError(kernelApiStore.refreshConfig)\n  await ignoredError(kernelApiStore.refreshProviderProxies)\n  await sleep(100)\n  loading.value = false\n}\n\nconst locateGroup = (group: any, chain: string) => {\n  collapseAll()\n  if (kernelApiStore.proxies[chain]?.all) {\n    toggleExpanded(kernelApiStore.proxies[chain].name)\n  } else {\n    toggleExpanded(group.name)\n  }\n}\n\nconst delayColor = (delay = 0) => {\n  if (delay === 0) return 'var(--level-0-color)'\n  if (delay < 500) return 'var(--level-1-color)'\n  if (delay < 1000) return 'var(--level-2-color)'\n  if (delay < 1500) return 'var(--level-3-color)'\n  return 'var(--level-4-color)'\n}\n\nconst handleResetMoreSettings = () => {\n  appSettings.app.kernel.testUrl = DefaultTestURL\n  appSettings.app.kernel.testTimeout = DefaultTestTimeout\n  appSettings.app.kernel.concurrencyLimit = DefaultConcurrencyLimit\n  appSettings.app.kernel.controllerCloseMode = ControllerCloseMode.All\n  appSettings.app.kernel.controllerSensitivity = DefaultControllerSensitivity\n  appSettings.app.kernel.cardColumns = DefaultCardColumns\n  message.success('common.success')\n}\n\nonActivated(() => {\n  kernelApiStore.refreshProviderProxies()\n})\n</script>\n\n<template>\n  <div class=\"m-8 mt-0 sticky top-0 z-3\">\n    <div\n      class=\"sticky flex gap-8 items-center p-8 rounded-8 backdrop-blur-sm\"\n      style=\"background-color: var(--card-bg)\"\n    >\n      <Switch v-model=\"appSettings.app.kernel.autoClose\" label=\"home.controller.autoClose\" />\n      <Switch v-model=\"appSettings.app.kernel.unAvailable\" label=\"home.controller.unAvailable\" />\n      <Switch v-model=\"appSettings.app.kernel.cardMode\" label=\"home.controller.cardMode\" />\n      <Switch v-model=\"appSettings.app.kernel.sortByDelay\" label=\"home.controller.sortBy\" />\n      <Button type=\"primary\" size=\"small\" @click=\"toggleMoreSettings\"> ... </Button>\n      <div class=\"ml-auto flex items-center\">\n        <Button v-tips=\"'home.overview.expandAll'\" type=\"text\" icon=\"expand\" @click=\"expandAll\" />\n        <Button\n          v-tips=\"'home.overview.collapseAll'\"\n          type=\"text\"\n          icon=\"collapse\"\n          @click=\"collapseAll\"\n        />\n        <Button\n          v-tips=\"'home.overview.refresh'\"\n          :loading=\"loading\"\n          icon=\"refresh\"\n          type=\"text\"\n          @click=\"handleRefresh\"\n        />\n      </div>\n    </div>\n  </div>\n  <div v-for=\"group in groups\" :key=\"group.name\" class=\"m-8\">\n    <div\n      class=\"sticky z-2 flex gap-8 items-center p-8 rounded-8 backdrop-blur-sm\"\n      style=\"top: 52px; background-color: var(--card-bg)\"\n      @click=\"toggleExpanded(group.name)\"\n    >\n      <div class=\"text-14 flex items-center gap-2 text-nowrap overflow-hidden\">\n        <span class=\"font-bold text-18\">{{ group.name }}</span>\n        <span class=\"mx-8\">\n          {{ group.type }}\n        </span>\n        <span> :: </span>\n        <template v-for=\"(chain, index) in group.chains\" :key=\"chain\">\n          <span v-if=\"index !== 0\" style=\"color: gray\"> / </span>\n          <Button type=\"text\" size=\"small\" @click.stop=\"locateGroup(group, chain)\">\n            {{ chain }}\n          </Button>\n        </template>\n      </div>\n      <div class=\"ml-auto flex items-center\" @click.stop>\n        <Input\n          v-model=\"filterKeywordsMap[group.name]\"\n          :placeholder=\"t('common.keywords')\"\n          editable\n          clearable\n        >\n          <template #editable>\n            <Button\n              type=\"text\"\n              icon=\"filter\"\n              :icon-color=\"isFiltered(group.name) ? 'var(--primary-color)' : ''\"\n            />\n          </template>\n        </Input>\n        <Button\n          v-tips=\"'home.overview.delayTest'\"\n          :loading=\"isLoading(group.name)\"\n          icon=\"speedTest\"\n          type=\"text\"\n          @click=\"handleGroupDelay(group.name)\"\n        />\n        <Button type=\"text\" @click=\"toggleExpanded(group.name)\">\n          <Icon\n            :class=\"{ 'action-expand-expanded': isExpanded(group.name) }\"\n            class=\"action-expand origin-center duration-200\"\n            icon=\"arrowDown\"\n          />\n        </Button>\n      </div>\n    </div>\n    <Transition name=\"expand\">\n      <div v-if=\"isExpanded(group.name)\" class=\"py-8 px-4\">\n        <Empty v-if=\"group.all.length === 0\" />\n        <div\n          v-else-if=\"appSettings.app.kernel.cardMode\"\n          :class=\"`grid-cols-${appSettings.app.kernel.cardColumns}`\"\n          class=\"grid gap-8\"\n        >\n          <Card\n            v-for=\"proxy in group.all\"\n            :key=\"proxy.name\"\n            :title=\"proxy.name\"\n            :selected=\"proxy.name === group.now\"\n            class=\"cursor-pointer\"\n            @click=\"useProxyWithCatchError(group, proxy)\"\n          >\n            <Button\n              :style=\"{ color: delayColor(proxy.delay) }\"\n              :loading=\"isLoading(proxy.name)\"\n              type=\"text\"\n              size=\"small\"\n              style=\"margin-left: -2px; padding-left: 2px\"\n              @click.stop=\"handleProxyDelay(proxy.name)\"\n            >\n              <div class=\"text-12\">\n                {{ proxy.delay && proxy.delay + 'ms' }}\n              </div>\n            </Button>\n            <div class=\"text-12 my-2\">{{ proxy.type }} {{ proxy.udp ? ':: udp' : '' }}</div>\n          </Card>\n        </div>\n        <div v-else class=\"grid grid-cols-32 gap-8\">\n          <div\n            v-for=\"proxy in group.all\"\n            :key=\"proxy.name\"\n            v-tips.fast=\"proxy.name\"\n            :style=\"{ background: delayColor(proxy.delay) }\"\n            :class=\"proxy.name === group.now ? 'rounded-full shadow' : ''\"\n            class=\"w-12 h-12 rounded-4 flex items-center justify-center\"\n            @click=\"useProxyWithCatchError(group, proxy)\"\n          >\n            <Icon v-if=\"isLoading(proxy.name)\" icon=\"loading\" :size=\"12\" class=\"rotation\" />\n          </div>\n        </div>\n      </div>\n    </Transition>\n  </div>\n\n  <Modal\n    v-model:open=\"showMoreSettings\"\n    :submit=\"false\"\n    mask-closable\n    cancel-text=\"common.close\"\n    title=\"common.more\"\n  >\n    <template #action>\n      <Button type=\"text\" class=\"mr-auto\" @click=\"handleResetMoreSettings\">\n        {{ t('common.reset') }}\n      </Button>\n    </template>\n\n    <div class=\"form-item\">\n      {{ t('home.controller.delay') }}\n      <Input\n        v-model=\"appSettings.app.kernel.testUrl\"\n        :placeholder=\"DefaultTestURL\"\n        editable\n        clearable\n      />\n    </div>\n\n    <div class=\"form-item\">\n      {{ t('home.controller.timeout') }}\n      <Input\n        v-model=\"appSettings.app.kernel.testTimeout\"\n        :placeholder=\"String(DefaultTestTimeout)\"\n        type=\"number\"\n        editable\n        clearable\n      />\n    </div>\n\n    <div class=\"form-item\">\n      {{ t('home.controller.concurrencyLimit') }}\n      <Input\n        v-model=\"appSettings.app.kernel.concurrencyLimit\"\n        :min=\"1\"\n        :max=\"50\"\n        type=\"number\"\n        editable\n        clearable\n      />\n    </div>\n\n    <div class=\"form-item\">\n      {{ t('home.controller.closeMode.name') }}\n      <Radio\n        v-model=\"appSettings.app.kernel.controllerCloseMode\"\n        :options=\"ControllerCloseModeOptions\"\n      />\n    </div>\n\n    <div\n      v-if=\"appSettings.app.kernel.controllerCloseMode === ControllerCloseMode.All\"\n      class=\"form-item\"\n    >\n      {{ t('home.controller.sensitivity') }}\n      <Input\n        v-model=\"appSettings.app.kernel.controllerSensitivity\"\n        type=\"number\"\n        :min=\"1\"\n        :max=\"6\"\n        placeholder=\"1-6\"\n        editable\n      />\n    </div>\n\n    <div class=\"form-item\">\n      {{ t('home.controller.cardColumns') }}\n      <Radio\n        v-model=\"appSettings.app.kernel.cardColumns\"\n        :options=\"Array.from({ length: 5 }, (_, i) => ({ label: String(i + 1), value: i + 1 }))\"\n      />\n    </div>\n  </Modal>\n</template>\n\n<style lang=\"less\" scoped>\n.expand-enter-active,\n.expand-leave-active {\n  transform-origin: top;\n  transition:\n    transform 0.2s ease-in-out,\n    opacity 0.2s ease-in-out;\n}\n\n.expand-enter-from,\n.expand-leave-to {\n  transform: scaleY(0);\n}\n\n.action-expand {\n  transform: rotate(-90deg);\n  &-expanded {\n    transform: rotate(0deg);\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/HomeView/components/KernelLogs.vue",
    "content": "<script setup lang=\"ts\">\nimport { h, withDirectives } from 'vue'\n\nimport { ClipboardSetText } from '@/bridge'\nimport vTips from '@/directives/tips'\nimport { useLogsStore } from '@/stores'\nimport { message } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\nconst logsStore = useLogsStore()\n\nconst modalSlots = {\n  toolbar: () =>\n    withDirectives(\n      h(Button, {\n        type: 'text',\n        icon: 'file',\n        onClick: async () => {\n          if (logsStore.isEmpty) return\n          await ClipboardSetText(logsStore.kernelLogs.join('\\n'))\n          message.success('common.success')\n        },\n      }),\n      [[vTips, 'common.copy']],\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div class=\"h-full overflow-y-auto\">\n    <Empty v-if=\"logsStore.isEmpty\" description=\"home.overview.noLogs\" />\n    <template v-else>\n      <div\n        v-for=\"(log, i) in logsStore.kernelLogs\"\n        :key=\"i\"\n        :style=\"{\n          background: 'var(--card-bg)',\n        }\"\n        class=\"text-12 my-4 py-2 px-4\"\n      >\n        {{ log }}\n      </div>\n    </template>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/HomeView/components/LogsController.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, computed, onUnmounted } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { LogLevelOptions } from '@/constant/kernel'\nimport { useBool } from '@/hooks'\nimport { useKernelApiStore } from '@/stores'\nimport { addToRuleSet, buildSmartRegExp, isValidIPv4, isValidIPv6, message, picker } from '@/utils'\n\nimport type { PickerItem } from '@/components/Picker/index.vue'\nimport type { Menu } from '@/types/app'\n\nconst logType = ref<'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'panic'>('info')\nconst keywords = ref('')\nconst logs = ref<{ type: string; payload: string }[]>([])\n\nconst LogLevelMap = {\n  trace: ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic'],\n  debug: ['debug', 'info', 'warn', 'error', 'fatal', 'panic'],\n  info: ['info', 'warn', 'error', 'fatal', 'panic'],\n  warn: ['warn', 'error', 'fatal', 'panic'],\n  error: ['error', 'fatal', 'panic'],\n  fatal: ['fatal', 'panic'],\n  panic: ['panic'],\n}\n\nconst filteredLogs = computed(() => {\n  return logs.value.filter((v) => {\n    const hitType = LogLevelMap[logType.value].includes(v.type)\n    const hitName = buildSmartRegExp(keywords.value, 'i').test(v.payload)\n    return hitName && hitType\n  })\n})\n\nconst menus: Menu[] = (\n  [\n    ['home.connections.addToDirect', 'direct'],\n    ['home.connections.addToProxy', 'proxy'],\n    ['home.connections.addToReject', 'reject'],\n  ] as const\n).map(([label, ruleset]) => {\n  return {\n    label,\n    handler: async ({ type, payload }) => {\n      if (type !== 'info') {\n        message.error('Not Support')\n        return\n      }\n      const regex = /(\\b((?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|(?:\\d{1,3}\\.){3}\\d{1,3})(:\\d+)?\\b)/g\n      const matches = payload.match(regex)\n      if (!matches) {\n        message.error('Not Matched')\n        return\n      }\n\n      const options: PickerItem<Record<string, any>[]>[] = []\n\n      matches.forEach((match: string) => {\n        // FIXME: IPv6\n        const address = match.split(':')[0]\n        if (!address) return\n        if (isValidIPv4(address) || isValidIPv6(address)) {\n          options.push({\n            label: t('kernel.rules.type.ip_cidr'),\n            value: { ip_cidr: address + '/32' } as any,\n            description: address,\n          })\n        } else {\n          options.push({\n            label: t('kernel.rules.type.domain'),\n            value: { domain: address } as any,\n            description: address,\n          })\n        }\n      })\n\n      const payloads = await picker.multi('rulesets.selectRuleType', options)\n\n      try {\n        await addToRuleSet(ruleset, payloads)\n        message.success('common.success')\n      } catch (error: any) {\n        message.error(error)\n        console.log(error)\n      }\n    },\n  }\n})\n\nconst { t } = useI18n()\nconst [pause, togglePause] = useBool(false)\nconst kernelApiStore = useKernelApiStore()\n\nconst handleClear = () => logs.value.splice(0)\n\nconst unregisterLogsHandler = kernelApiStore.onLogs((data) => {\n  pause.value || logs.value.unshift(data)\n})\n\nonUnmounted(() => {\n  unregisterLogsHandler()\n})\n</script>\n\n<template>\n  <div class=\"flex flex-col h-full\">\n    <div class=\"flex items-center\">\n      <span class=\"text-12 pr-8\">\n        {{ t('kernel.log.level') }}\n        :\n      </span>\n      <Select v-model=\"logType\" :options=\"LogLevelOptions\" size=\"small\" />\n      <Input\n        v-model=\"keywords\"\n        clearable\n        size=\"small\"\n        :placeholder=\"t('common.keywords')\"\n        class=\"ml-8 flex-1\"\n      />\n      <Button\n        :icon=\"pause ? 'play' : 'pause'\"\n        type=\"text\"\n        size=\"small\"\n        class=\"ml-8\"\n        @click=\"togglePause\"\n      />\n      <Button v-tips=\"'common.clear'\" icon=\"clear\" size=\"small\" type=\"text\" @click=\"handleClear\" />\n    </div>\n\n    <Empty v-if=\"filteredLogs.length === 0\" />\n\n    <div v-else class=\"mt-8 overflow-y-auto\">\n      <div\n        v-for=\"log in filteredLogs\"\n        :key=\"log.payload\"\n        v-menu=\"menus.map((v) => ({ ...v, handler: () => v.handler?.(log) }))\"\n        class=\"log select-text text-12 py-2 px-4 my-4\"\n      >\n        <span class=\"type inline-block text-center\">{{ log.type }}</span> {{ log.payload }}\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"less\" scoped>\n.log {\n  background: var(--card-bg);\n  &:hover {\n    color: #fff;\n    background: var(--primary-color);\n    .type {\n      color: #fff;\n    }\n  }\n}\n\n.type {\n  width: 50px;\n  color: var(--primary-color);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/HomeView/components/OverView.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, onUnmounted, defineAsyncComponent } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { ProcessMemory } from '@/bridge'\nimport { ModeOptions } from '@/constant/kernel'\nimport { useEnvStore, useAppStore, useKernelApiStore, useAppSettingsStore } from '@/stores'\nimport { formatBytes, handleChangeMode, message } from '@/utils'\n\nimport { useModal } from '@/components/Modal'\n\nconst CommonController = defineAsyncComponent(() => import('./CommonController.vue'))\nconst ConnectionsController = defineAsyncComponent(() => import('./ConnectionsController.vue'))\nconst LogsController = defineAsyncComponent(() => import('./LogsController.vue'))\n\nconst trafficHistory = ref<[number[], number[]]>([[], []])\nconst statistics = ref({\n  upload: 0,\n  download: 0,\n  downloadTotal: 0,\n  uploadTotal: 0,\n  connections: [] as any[],\n  inuse: 0,\n  memUsage: 0,\n})\n\nconst { t } = useI18n()\nconst [Modal, modalApi] = useModal({})\nconst appStore = useAppStore()\nconst envStore = useEnvStore()\nconst appSettings = useAppSettingsStore()\nconst kernelApiStore = useKernelApiStore()\n\nconst handleRestartKernel = async () => {\n  try {\n    await kernelApiStore.restartCore()\n  } catch (error: any) {\n    console.error(error)\n    message.error(error)\n  }\n}\n\nconst handleStopKernel = async () => {\n  try {\n    await kernelApiStore.stopCore()\n  } catch (error: any) {\n    console.error(error)\n    message.error(error)\n  }\n}\n\nconst handleShowApiLogs = () => {\n  modalApi.setProps({\n    title: 'Logs',\n    cancelText: 'common.close',\n    width: '90',\n    height: '90',\n    submit: false,\n    maskClosable: true,\n  })\n  modalApi.setContent(LogsController).open()\n}\n\nconst handleShowApiConnections = () => {\n  modalApi.setProps({\n    title: 'home.overview.connections',\n    cancelText: 'common.close',\n    width: '90',\n    height: '90',\n    submit: false,\n    maskClosable: true,\n  })\n  modalApi.setContent(ConnectionsController).open()\n}\n\nconst handleToggleRealMemoryUsage = () => {\n  appSettings.app.kernel.realMemoryUsage = !appSettings.app.kernel.realMemoryUsage\n}\n\nconst handleShowSettings = () => {\n  modalApi.setProps({\n    title: 'home.overview.settings',\n    cancelText: 'common.close',\n    width: '90',\n    submit: false,\n    maskClosable: true,\n  })\n  modalApi.setContent(CommonController).open()\n}\n\nconst onTunSwitchChange = async (enable: boolean) => {\n  try {\n    await kernelApiStore.updateConfig('tun', { enable })\n  } catch (error: any) {\n    kernelApiStore.config.tun.enable = !kernelApiStore.config.tun.enable\n    console.error(error)\n    message.error(error)\n  }\n}\n\nconst onSystemProxySwitchChange = async (enable: boolean) => {\n  try {\n    await envStore.switchSystemProxy(enable)\n  } catch (error: any) {\n    console.error(error)\n    message.error(error)\n    envStore.systemProxy = !envStore.systemProxy\n  }\n}\n\nlet latestCoreMemoryUsageTime: number\nconst getCoreMemoryUsage = async (fallback: number) => {\n  if (latestCoreMemoryUsageTime && Date.now() - latestCoreMemoryUsageTime < 3_000) {\n    return fallback\n  }\n  const useage = await ProcessMemory(kernelApiStore.pid).catch(() => fallback)\n  latestCoreMemoryUsageTime = Date.now()\n  return useage\n}\n\nconst unregisterMemoryHandler = kernelApiStore.onMemory(async (data) => {\n  statistics.value.inuse = data.inuse\n  if (appSettings.app.kernel.realMemoryUsage) {\n    getCoreMemoryUsage(statistics.value.memUsage || data.inuse).then((usage) => {\n      statistics.value.memUsage = usage\n    })\n  }\n})\n\nconst unregisterTrafficHandler = kernelApiStore.onTraffic((data) => {\n  const { up, down } = data\n  statistics.value.upload = up\n  statistics.value.download = down\n\n  trafficHistory.value[0].push(up)\n  trafficHistory.value[1].push(down)\n\n  if (trafficHistory.value[0].length > 60) {\n    trafficHistory.value[0].shift()\n    trafficHistory.value[1].shift()\n  }\n})\n\nconst unregisterConnectionsHandler = kernelApiStore.onConnections((data) => {\n  statistics.value.downloadTotal = data.downloadTotal\n  statistics.value.uploadTotal = data.uploadTotal\n  statistics.value.connections = data.connections || []\n})\n\nonUnmounted(() => {\n  unregisterMemoryHandler()\n  unregisterTrafficHandler()\n  unregisterConnectionsHandler()\n})\n</script>\n\n<template>\n  <div>\n    <div class=\"flex items-center rounded-8 px-8 py-4\" style=\"background-color: var(--card-bg)\">\n      <Button type=\"text\" size=\"small\" icon=\"settings\" @click=\"handleShowSettings\" />\n      <Switch\n        v-model=\"envStore.systemProxy\"\n        size=\"small\"\n        border=\"square\"\n        class=\"ml-4\"\n        @change=\"onSystemProxySwitchChange\"\n      >\n        {{ t('home.overview.systemProxy') }}\n      </Switch>\n      <Switch\n        v-model=\"kernelApiStore.config.tun.enable\"\n        size=\"small\"\n        border=\"square\"\n        class=\"ml-8\"\n        @change=\"onTunSwitchChange\"\n      >\n        {{ t('home.overview.tunMode') }}\n      </Switch>\n      <CustomAction :actions=\"appStore.customActions.core_state\" />\n      <Button\n        v-tips=\"'home.overview.viewlog'\"\n        type=\"text\"\n        size=\"small\"\n        icon=\"log\"\n        class=\"ml-auto\"\n        @click=\"handleShowApiLogs\"\n      />\n      <Button\n        v-tips=\"'home.overview.restart'\"\n        :loading=\"kernelApiStore.restarting\"\n        type=\"text\"\n        size=\"small\"\n        icon=\"restart\"\n        @click=\"handleRestartKernel\"\n      />\n      <Button\n        v-tips=\"'home.overview.stop'\"\n        :loading=\"kernelApiStore.stopping\"\n        type=\"text\"\n        size=\"small\"\n        icon=\"stop\"\n        @click=\"handleStopKernel\"\n      />\n    </div>\n    <div class=\"flex mt-20 gap-12\">\n      <Card :title=\"t('home.overview.realtimeTraffic')\" class=\"flex-1\">\n        <div class=\"py-8 text-12\">\n          ↑ {{ formatBytes(statistics.upload) }}/s ↓ {{ formatBytes(statistics.download) }}/s\n        </div>\n      </Card>\n      <Card :title=\"t('home.overview.totalTraffic')\" class=\"flex-1\">\n        <div class=\"py-8 text-12\">\n          ↑ {{ formatBytes(statistics.uploadTotal) }} ↓ {{ formatBytes(statistics.downloadTotal) }}\n        </div>\n      </Card>\n      <Card\n        :title=\"t('home.overview.connections')\"\n        class=\"flex-1 cursor-pointer\"\n        @click=\"handleShowApiConnections\"\n      >\n        <div class=\"py-8 text-12\">\n          {{ statistics.connections.length }}\n        </div>\n      </Card>\n      <Card\n        :title=\"t('home.overview.memory')\"\n        class=\"flex-1 cursor-pointer\"\n        @click=\"handleToggleRealMemoryUsage\"\n      >\n        <div class=\"py-8 text-12\">\n          {{ formatBytes(statistics.inuse) }}\n          <span v-if=\"appSettings.app.kernel.realMemoryUsage\">\n            / ({{ formatBytes(statistics.memUsage) }})\n          </span>\n        </div>\n      </Card>\n    </div>\n    <div class=\"flex\">\n      <div class=\"w-[60%]\">\n        <div class=\"py-16 font-bold\" style=\"color: var(--card-color)\">\n          {{ t('home.overview.traffic') }}\n        </div>\n        <TrafficChart\n          :series=\"trafficHistory\"\n          :legend=\"[t('home.overview.transmit'), t('home.overview.receive')]\"\n        />\n      </div>\n      <div class=\"ml-12 flex-1\">\n        <div class=\"py-16 font-bold\" style=\"color: var(--card-color)\">\n          {{ t('kernel.mode') }}\n        </div>\n        <div class=\"flex flex-col gap-12\">\n          <Card\n            v-for=\"mode in ModeOptions\"\n            :key=\"mode.value\"\n            :selected=\"kernelApiStore.config.mode === mode.value\"\n            :title=\"t(mode.label)\"\n            class=\"cursor-pointer\"\n            @click=\"handleChangeMode(mode.value as any)\"\n          >\n            <div class=\"text-12 py-2\">{{ t(mode.desc) }}</div>\n          </Card>\n        </div>\n      </div>\n    </div>\n  </div>\n\n  <Modal />\n</template>\n"
  },
  {
    "path": "frontend/src/views/HomeView/components/QuickStart.vue",
    "content": "<script setup lang=\"ts\">\nimport { h, inject, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useProfilesStore, useAppSettingsStore, useSubscribesStore } from '@/stores'\nimport { message, sampleID } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\nconst { t } = useI18n()\nconst subscribeStore = useSubscribesStore()\nconst profilesStore = useProfilesStore()\nconst appSettingsStore = useAppSettingsStore()\n\nconst url = ref('')\nconst name = ref('')\nconst loading = ref(false)\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst handleSave = async () => {\n  if (!name.value) {\n    name.value = sampleID()\n  }\n\n  const sub = subscribeStore.getSubscribeTemplate(name.value, { url: url.value })\n\n  loading.value = true\n\n  try {\n    await subscribeStore.addSubscribe(sub)\n    await subscribeStore.updateSubscribe(sub.id)\n  } catch (error: any) {\n    loading.value = false\n    console.log(error)\n    message.error(error)\n    subscribeStore.deleteSubscribe(sub.id)\n    return\n  }\n\n  const profile = profilesStore.getProfileTemplate(name.value)\n\n  if (profile.outbounds[0] && profile.outbounds[1]) {\n    profile.outbounds[0].outbounds.push({ id: sub.id, tag: sub.id, type: 'Subscription' })\n    profile.outbounds[1].outbounds.push({ id: sub.id, tag: sub.id, type: 'Subscription' })\n  }\n\n  await profilesStore.addProfile(profile)\n\n  appSettingsStore.app.kernel.profile = profile.id\n\n  message.success('home.initSuccessful')\n\n  loading.value = false\n\n  handleSubmit()\n}\n\nconst modalSlots = {\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        disabled: !/^https?:\\/\\//.test(url.value),\n        loading: loading.value,\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div class=\"flex gap-4\">\n    <Input v-model=\"name\" :placeholder=\"$t('profile.name')\" auto-size clearable class=\"w-[25%]\" />\n    <Input v-model=\"url\" placeholder=\"http(s)://\" autofocus clearable  allow-paste class=\"w-[75%]\" />\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/HomeView/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, watch, useTemplateRef, defineAsyncComponent } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport logo from '@/assets/logo'\nimport { ControllerCloseMode } from '@/enums/app'\nimport { useAppSettingsStore, useProfilesStore, useKernelApiStore } from '@/stores'\nimport { APP_TITLE, debounce, message } from '@/utils'\n\nimport { useModal } from '@/components/Modal'\n\nconst OverView = defineAsyncComponent(() => import('./components/OverView.vue'))\nconst QuickStart = defineAsyncComponent(() => import('./components/QuickStart.vue'))\nconst KernelLogs = defineAsyncComponent(() => import('./components/KernelLogs.vue'))\nconst GroupsController = defineAsyncComponent(() => import('./components/GroupsController.vue'))\n\nconst showController = ref(false)\nconst controllerRef = useTemplateRef('controllerRef')\n\nconst { t } = useI18n()\nconst [Modal, modalApi] = useModal({})\n\nconst appSettingsStore = useAppSettingsStore()\nconst profilesStore = useProfilesStore()\nconst kernelApiStore = useKernelApiStore()\n\nconst handleStartKernel = async () => {\n  try {\n    await kernelApiStore.startCore()\n  } catch (error: any) {\n    console.error(error)\n    message.error(error.message || error)\n  }\n}\n\nconst handleShowQuickStart = () => {\n  modalApi.setProps({ title: 'subscribes.enterLink' })\n  modalApi.setContent(QuickStart).open()\n}\n\nconst handleShowKernelLogs = () => {\n  modalApi.setProps({\n    title: 'home.overview.viewlog',\n    width: '90',\n    height: '90',\n    submit: false,\n    cancelText: 'common.close',\n    maskClosable: true,\n  })\n  modalApi.setContent(KernelLogs).open()\n}\n\nlet scrollEventCount = 0\nconst resetScrollEventCount = debounce(() => (scrollEventCount = 0), 100)\n\nconst onMouseWheel = (e: WheelEvent) => {\n  if (!kernelApiStore.running) return\n\n  const isScrollingDown = e.deltaY > 0\n\n  if (\n    isScrollingDown ||\n    appSettingsStore.app.kernel.controllerCloseMode === ControllerCloseMode.All\n  ) {\n    const currentScrollTop = controllerRef.value?.scrollTop ?? 0\n    if (isScrollingDown || currentScrollTop === 0) {\n      scrollEventCount += 1\n    }\n    if (scrollEventCount >= appSettingsStore.app.kernel.controllerSensitivity) {\n      showController.value = isScrollingDown || currentScrollTop !== 0\n    }\n  }\n\n  resetScrollEventCount()\n}\n\nwatch(showController, (v) => {\n  if (v) {\n    kernelApiStore.refreshProviderProxies()\n  } else {\n    kernelApiStore.refreshConfig()\n  }\n})\n</script>\n\n<template>\n  <div class=\"relative overflow-hidden h-full\" @wheel.passive=\"onMouseWheel\">\n    <div\n      v-if=\"(!kernelApiStore.running && !kernelApiStore.stopping) || kernelApiStore.starting\"\n      class=\"w-full h-[90%] flex flex-col items-center justify-center\"\n    >\n      <img :src=\"logo\" draggable=\"false\" class=\"w-128 mb-16\" />\n\n      <template v-if=\"profilesStore.profiles.length === 0\">\n        <p>{{ t('home.noProfile', [APP_TITLE]) }}</p>\n        <Button type=\"primary\" @click=\"handleShowQuickStart\">{{ t('home.quickStart') }}</Button>\n      </template>\n\n      <template v-else>\n        <div class=\"flex gap-8 mb-32\">\n          <Card\n            v-for=\"p in profilesStore.profiles.slice(0, profilesStore.profiles.length > 4 ? 3 : 4)\"\n            :key=\"p.id\"\n            :selected=\"appSettingsStore.app.kernel.profile === p.id\"\n            @click=\"appSettingsStore.app.kernel.profile = p.id\"\n          >\n            <div\n              class=\"w-128 h-full flex items-center justify-center py-24 text-center cursor-pointer font-bold text-12\"\n            >\n              {{ p.name }}\n            </div>\n          </Card>\n          <Dropdown v-if=\"profilesStore.profiles.length > 4\" placement=\"top\">\n            <Card class=\"h-full\">\n              <div\n                class=\"w-128 h-full flex items-center justify-center py-24 text-center cursor-pointer font-bold text-12\"\n              >\n                ...\n              </div>\n            </Card>\n            <template #overlay>\n              <div class=\"flex flex-col py-8\">\n                <Button\n                  v-for=\"p in profilesStore.profiles.slice(3)\"\n                  :key=\"p.id\"\n                  @click=\"appSettingsStore.app.kernel.profile = p.id\"\n                >\n                  <div class=\"min-w-32 w-full flex items-center justify-between\">\n                    {{ p.name }}\n                    <Icon v-if=\"appSettingsStore.app.kernel.profile === p.id\" icon=\"selected\" />\n                  </div>\n                </Button>\n              </div>\n            </template>\n          </Dropdown>\n          <Card @click=\"handleShowQuickStart\">\n            <div\n              class=\"w-128 h-full flex items-center justify-center py-24 text-center cursor-pointer font-bold text-12\"\n            >\n              {{ t('home.quickStart') }}\n            </div>\n          </Card>\n        </div>\n        <Button :loading=\"kernelApiStore.starting\" type=\"primary\" @click=\"handleStartKernel\">\n          {{ t('home.overview.start') }}\n        </Button>\n        <Button type=\"link\" size=\"small\" class=\"mt-4\" @click=\"handleShowKernelLogs\">\n          {{ t('home.overview.viewlog') }}\n        </Button>\n      </template>\n    </div>\n\n    <template v-else-if=\"!kernelApiStore.coreStateLoading\">\n      <div :class=\"{ 'blur-3xl': showController }\">\n        <OverView />\n        <Divider>\n          <Button type=\"link\" size=\"small\" @click=\"showController = true\">\n            {{ t('home.controller.name') }}\n          </Button>\n        </Divider>\n      </div>\n\n      <div\n        ref=\"controllerRef\"\n        :class=\"showController ? 'translate-y-0' : 'translate-y-full'\"\n        class=\"absolute inset-0 pb-32 overflow-y-auto duration-400\"\n      >\n        <GroupsController />\n      </div>\n\n      <Button\n        v-show=\"showController\"\n        class=\"fixed left-1/2 -translate-x-1/2 bottom-12 z-2\"\n        style=\"background-color: var(--card-bg)\"\n        type=\"text\"\n        size=\"small\"\n        icon=\"close\"\n        @click=\"showController = false\"\n      />\n    </template>\n  </div>\n\n  <Modal />\n</template>\n"
  },
  {
    "path": "frontend/src/views/PluginsView/components/PluginChangelog.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nimport { HttpGet, ReadFile } from '@/bridge'\nimport { usePluginsStore } from '@/stores'\nimport { ignoredError } from '@/utils'\n\ninterface Props {\n  id: string\n}\n\nconst props = defineProps<Props>()\n\nconst code = ref('')\n\nconst pluginsStore = usePluginsStore()\n\nconst fetchAndUpdatePluginCode = async () => {\n  const p = pluginsStore.getPluginById(props.id)\n  if (p) {\n    const _code = pluginsStore.getPluginCodefromCache(p.id)\n    if (_code) {\n      code.value = _code\n    } else {\n      const content = (await ignoredError(ReadFile, p.path)) || ''\n      code.value = content\n    }\n    const { body } = await HttpGet(p.url)\n    code.value = body\n  }\n}\n\nfetchAndUpdatePluginCode()\n</script>\n\n<template>\n  <CodeViewer v-model=\"code\" lang=\"javascript\" mode=\"diff\" />\n</template>\n"
  },
  {
    "path": "frontend/src/views/PluginsView/components/PluginConfigItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, watch } from 'vue'\n\nimport { deepClone, message } from '@/utils'\n\nimport type { Plugin } from '@/types/app'\n\ninterface Props {\n  plugin: Plugin\n  modelValue?: Recordable\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  modelValue: () => ({}),\n})\n\nconst emit = defineEmits(['change', 'update:modelValue'])\n\nconst model = ref(deepClone(props.modelValue ?? {}))\n\nlet internalUpdate = false\n\nwatch(\n  () => props.modelValue,\n  (val) => {\n    if (!internalUpdate) {\n      model.value = val\n    }\n    internalUpdate = false\n  },\n  { deep: true },\n)\n\nconst getOptions = (val: string[]) => {\n  return val.map((v) => {\n    const arr = v.split(',')\n    return { label: arr[0], value: arr[1] || arr[0] }\n  })\n}\n\nconst emitUpdate = () => {\n  const val = deepClone(model.value)\n  emit('update:modelValue', val)\n  emit('change', val)\n  internalUpdate = true\n}\n\nconst onChange = (key: string, originalValue: any, value: any) => {\n  // TODO: array order\n  if (JSON.stringify(originalValue) === JSON.stringify(value)) {\n    delete model.value[key]\n  } else {\n    model.value[key] = value\n  }\n  emitUpdate()\n}\n\nconst handleReset = (key: string) => {\n  delete model.value[key]\n  emitUpdate()\n}\n\nconst handleResetAll = () => {\n  model.value = {}\n  emitUpdate()\n  message.success('common.success')\n}\n\ndefineExpose({ reset: handleResetAll })\n</script>\n\n<template>\n  <div class=\"flex flex-col gap-8 pr-8\">\n    <slot name=\"header\" v-bind=\"{ handleResetAll }\"></slot>\n    <Card\n      v-for=\"(conf, index) in plugin.configuration\"\n      :key=\"conf.id\"\n      :title=\"`${index + 1}. ${conf.title}`\"\n      :class=\"{ warn: model[conf.key] !== undefined }\"\n      class=\"card\"\n    >\n      <template v-if=\"model[conf.key] !== undefined\" #extra>\n        <Button\n          v-tips=\"'settings.plugin.resetSetting'\"\n          :icon-size=\"12\"\n          icon=\"clear\"\n          type=\"text\"\n          size=\"small\"\n          @click=\"handleReset(conf.key)\"\n        />\n      </template>\n      <div class=\"mb-8 text-12\">{{ conf.description }}</div>\n      <Component\n        :is=\"conf.component\"\n        :model-value=\"model[conf.key] ?? conf.value\"\n        :options=\"getOptions(conf.options)\"\n        :autofocus=\"false\"\n        editable\n        lang=\"yaml\"\n        @change=\"(val: any) => onChange(conf.key, conf.value, val)\"\n      />\n    </Card>\n  </div>\n</template>\n\n<style scoped>\n.card {\n  border-left: 2px solid transparent;\n}\n.warn {\n  border-left: 2px solid var(--primary-color);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/PluginsView/components/PluginConfigurator.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, h, useTemplateRef } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { PluginTriggerEvent } from '@/enums/app'\nimport { usePluginsStore, useAppSettingsStore } from '@/stores'\nimport { message } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\nimport PluginConfigItem from './PluginConfigItem.vue'\n\nimport type { Plugin } from '@/types/app'\n\ninterface Props {\n  plugin: Plugin\n}\n\nconst props = defineProps<Props>()\n\nconst { t } = useI18n()\nconst pluginsStore = usePluginsStore()\nconst appSettingsStore = useAppSettingsStore()\nconst pluginConfigRef = useTemplateRef('pluginConfigRef')\n\nconst loading = ref(false)\nconst settings = ref(appSettingsStore.app.pluginSettings[props.plugin.id] ?? {})\nconst oldSettings = settings.value\nconst originalSettings = props.plugin.configuration.reduce((p, { key, value }) => {\n  p[key] = value\n  return p\n}, {} as Recordable)\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst handleSave = async () => {\n  loading.value = true\n  try {\n    await pluginsStore.manualTrigger(\n      props.plugin.id,\n      PluginTriggerEvent.OnConfigure,\n      Object.assign({}, originalSettings, settings.value),\n      Object.assign({}, originalSettings, oldSettings),\n    )\n  } catch (error: any) {\n    const errors = [\n      props.plugin.id + ' Not Found',\n      'is Missing source code',\n      'Disabled',\n      \"Can't find variable: \" + PluginTriggerEvent.OnConfigure,\n      PluginTriggerEvent.OnConfigure + ' is not defined',\n    ]\n    if (errors.every((v) => !error.includes(v))) {\n      message.error(error)\n      return\n    }\n  } finally {\n    loading.value = false\n  }\n\n  if (JSON.stringify(settings.value) === '{}') {\n    delete appSettingsStore.app.pluginSettings[props.plugin.id]\n  } else {\n    appSettingsStore.app.pluginSettings[props.plugin.id] = settings.value\n  }\n\n  await handleSubmit()\n  message.success('common.success')\n}\n\nconst modalSlots = {\n  action: () =>\n    h(\n      Button,\n      {\n        type: 'link',\n        class: 'mr-auto',\n        onClick: () => pluginConfigRef.value?.reset(),\n      },\n      () => t('plugin.restore'),\n    ),\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <PluginConfigItem ref=\"pluginConfigRef\" v-model=\"settings\" :plugin=\"props.plugin\" />\n</template>\n"
  },
  {
    "path": "frontend/src/views/PluginsView/components/PluginForm.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, computed, h } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { PluginsTriggerOptions, DraggableOptions } from '@/constant/app'\nimport { PluginTrigger } from '@/enums/app'\nimport { useBool } from '@/hooks'\nimport { usePluginsStore } from '@/stores'\nimport { deepClone, message, sampleID } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\nimport type { Plugin } from '@/types/app'\n\ninterface Props {\n  id?: string\n}\n\nconst props = defineProps<Props>()\n\nconst official = computed(() => pluginsStore.findPluginInHubById(plugin.value.id))\nconst loading = ref(false)\nconst pluginID = sampleID()\nconst plugin = ref<Plugin>({\n  id: pluginID,\n  version: 'v1.0.0',\n  name: '',\n  description: '',\n  tags: [],\n  type: 'File',\n  url: '',\n  status: 0,\n  path: `data/plugins/plugin-${pluginID}.js`,\n  triggers: [PluginTrigger.OnManual],\n  hasUI: false,\n  menus: {},\n  context: {\n    profiles: {},\n    subscriptions: {},\n    rulesets: {},\n    plugins: {},\n    scheduledtasks: {},\n  },\n  configuration: [],\n  disabled: false,\n  install: false,\n  installed: false,\n})\n\nconst componentList = [\n  'CheckBox',\n  'CodeViewer',\n  'Input',\n  'InputList',\n  'KeyValueEditor',\n  'Radio',\n  'Select',\n  'MultipleSelect',\n  'Switch',\n  'ColorPicker',\n] as const\n\ntype ComponentType = (typeof componentList)[number]\n\nconst { t } = useI18n()\nconst [showMore, toggleShowMore] = useBool(false)\nconst pluginsStore = usePluginsStore()\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst handleRestore = () => {\n  if (official.value) {\n    plugin.value = deepClone(official.value)\n    message.success('common.success')\n  }\n}\n\nconst handleSave = async () => {\n  loading.value = true\n  try {\n    if (props.id) {\n      await pluginsStore.editPlugin(props.id, plugin.value)\n    } else {\n      await pluginsStore.addPlugin(plugin.value)\n    }\n    await handleSubmit()\n  } catch (error: any) {\n    console.error(error)\n    message.error(error)\n  }\n  loading.value = false\n}\n\nconst handleAddParam = async () => {\n  plugin.value.configuration.push({\n    id: sampleID(),\n    title: '',\n    description: '',\n    key: '',\n    component: '',\n    value: [],\n    options: [],\n  })\n}\n\nconst handleDelParam = (index: number) => {\n  plugin.value.configuration.splice(index, 1)\n}\n\nconst hasOption = (component: ComponentType) => {\n  return (\n    component !== 'InputList' &&\n    ['CheckBox', 'InputList', 'Radio', 'Select', 'MultipleSelect'].includes(component)\n  )\n}\n\nconst onComponentChange = (component: ComponentType, index: number) => {\n  switch (component) {\n    case 'CheckBox':\n    case 'InputList':\n    case 'MultipleSelect': {\n      plugin.value.configuration[index]!.value = []\n      plugin.value.configuration[index]!.options = []\n      break\n    }\n    case 'CodeViewer':\n    case 'Input':\n    case 'Radio':\n    case 'Select': {\n      plugin.value.configuration[index]!.value = ''\n      break\n    }\n    case 'ColorPicker': {\n      plugin.value.configuration[index]!.value = '#000000'\n      break\n    }\n    case 'KeyValueEditor': {\n      plugin.value.configuration[index]!.value = {}\n      break\n    }\n    case 'Switch': {\n      plugin.value.configuration[index]!.value = false\n      break\n    }\n  }\n  plugin.value.configuration[index]!.component = component\n}\n\nconst getOptions = (val: string[]) => {\n  return val.map((v) => {\n    const arr = v.split(',')\n    return { label: arr[0], value: arr[1] || arr[0] }\n  })\n}\n\nif (props.id) {\n  const p = pluginsStore.getPluginById(props.id)\n  p && (plugin.value = deepClone(p))\n}\n\nconst modalSlots = {\n  action: () =>\n    official.value\n      ? h(Button, { type: 'link', class: 'mr-auto', onClick: handleRestore }, () =>\n          t('plugin.restore'),\n        )\n      : undefined,\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        disabled:\n          !plugin.value.name ||\n          !plugin.value.version ||\n          !plugin.value.path ||\n          (plugin.value.type === 'Http' && !plugin.value.url),\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div class=\"w-full h-full\">\n    <div class=\"form-item\">\n      {{ t('plugin.type') }}\n      <Radio\n        v-model=\"plugin.type\"\n        :options=\"[\n          { label: 'common.http', value: 'Http' },\n          { label: 'common.file', value: 'File' },\n        ]\"\n      />\n    </div>\n    <div class=\"form-item\">\n      <div class=\"mr-8\">{{ t('plugin.trigger') }}</div>\n      <MultipleSelect v-model=\"plugin.triggers\" :options=\"PluginsTriggerOptions\" clearable />\n    </div>\n    <div class=\"form-item\">\n      {{ t('plugin.name') }} *\n      <div class=\"min-w-[75%]\">\n        <Input v-model=\"plugin.name\" autofocus class=\"w-full\" />\n      </div>\n    </div>\n    <div class=\"form-item\">\n      {{ t('plugin.version') }} *\n      <div class=\"min-w-[75%]\">\n        <Input v-model=\"plugin.version\" class=\"w-full\" />\n      </div>\n    </div>\n    <div v-show=\"plugin.type === 'Http'\" class=\"form-item\">\n      {{ t('plugin.url') }} *\n      <div class=\"min-w-[75%]\">\n        <Input\n          v-model=\"plugin.url\"\n          :placeholder=\"plugin.type === 'Http' ? 'http(s)://' : 'data/local/plugin-{filename}.js'\"\n          allow-paste\n          class=\"w-full\"\n        />\n      </div>\n    </div>\n    <div class=\"form-item\">\n      {{ t('plugin.path') }} *\n      <div class=\"min-w-[75%]\">\n        <Input\n          v-model=\"plugin.path\"\n          placeholder=\"data/plugins/plugin-{filename}.js\"\n          class=\"w-full\"\n        />\n      </div>\n    </div>\n    <div class=\"form-item\">\n      {{ t('plugin.description') }}\n      <div class=\"min-w-[75%]\">\n        <Input v-model=\"plugin.description\" class=\"w-full\" />\n      </div>\n    </div>\n    <Divider>\n      <Button type=\"text\" size=\"small\" @click=\"toggleShowMore\">\n        {{ t('common.more') }}\n      </Button>\n    </Divider>\n    <div v-show=\"showMore\" class=\"pb-8\">\n      <div class=\"form-item\">\n        {{ t('plugin.install') }}\n        <Switch v-model=\"plugin.install\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('plugin.hasUI') }}\n        <Switch v-model=\"plugin.hasUI\" />\n      </div>\n      <div class=\"form-item\" :class=\"{ 'items-start': Object.keys(plugin.menus).length !== 0 }\">\n        {{ t('plugin.menus') }}\n        <KeyValueEditor\n          v-model=\"plugin.menus\"\n          :placeholder=\"[t('plugin.menuKey'), t('plugin.menuValue')]\"\n        />\n      </div>\n      <div\n        :class=\"{ 'items-start': Object.keys(plugin.context.profiles).length !== 0 }\"\n        class=\"form-item\"\n      >\n        {{ t('plugin.context') }} - {{ t('router.profiles') }}\n        <KeyValueEditor\n          v-model=\"plugin.context.profiles\"\n          :placeholder=\"[t('plugin.menuKey'), t('plugin.menuValue')]\"\n        />\n      </div>\n      <div\n        :class=\"{ 'items-start': Object.keys(plugin.context.subscriptions).length !== 0 }\"\n        class=\"form-item\"\n      >\n        {{ t('plugin.context') }} - {{ t('router.subscriptions') }}\n        <KeyValueEditor\n          v-model=\"plugin.context.subscriptions\"\n          :placeholder=\"[t('plugin.menuKey'), t('plugin.menuValue')]\"\n        />\n      </div>\n      <Divider>{{ t('plugin.configuration') }}</Divider>\n      <div\n        v-draggable=\"[plugin.configuration, { ...DraggableOptions, handle: '.drag' }]\"\n        class=\"px-8 flex flex-col gap-8\"\n      >\n        <template v-for=\"(conf, index) in plugin.configuration\" :key=\"conf.id\">\n          <Card v-if=\"conf.component\" :title=\"conf.component\">\n            <template #title-prefix>\n              <Icon icon=\"drag\" class=\"drag cursor-move\" />\n              <div class=\"ml-8\">{{ index + 1 }}.</div>\n            </template>\n            <template #extra>\n              <Button size=\"small\" type=\"text\" @click=\"handleDelParam(index)\">\n                {{ t('common.delete') }}\n              </Button>\n            </template>\n            <div class=\"form-item\">\n              {{ t('plugin.confName') }}\n              <div class=\"min-w-[75%]\">\n                <Input v-model=\"conf.title\" placeholder=\"title\" class=\"w-full\" />\n              </div>\n            </div>\n            <div class=\"form-item\">\n              {{ t('plugin.confDescription') }}\n              <div class=\"min-w-[75%]\">\n                <Input v-model=\"conf.description\" placeholder=\"description\" class=\"w-full\" />\n              </div>\n            </div>\n            <div class=\"form-item\">\n              {{ t('plugin.confKey') }}\n              <div class=\"min-w-[75%]\">\n                <Input v-model=\"conf.key\" placeholder=\"key\" class=\"w-full\" />\n              </div>\n            </div>\n            <div class=\"form-item\" :class=\"{ 'items-start': conf.value.length !== 0 }\">\n              {{ t('plugin.confDefault') }}\n              <div :class=\"conf.component === 'CodeViewer' ? 'min-w-[75%]' : ''\">\n                <Component\n                  :is=\"conf.component\"\n                  v-model=\"conf.value\"\n                  :options=\"getOptions(conf.options)\"\n                  editable\n                  class=\"w-full\"\n                />\n              </div>\n            </div>\n            <div\n              v-if=\"hasOption(conf.component)\"\n              :class=\"{ 'items-start': conf.options.length !== 0 }\"\n              class=\"form-item\"\n            >\n              {{ t('plugin.options') }}\n              <InputList v-model=\"conf.options\" />\n            </div>\n          </Card>\n          <Card v-else :title=\"t('plugin.selectComponent')\">\n            <template #extra>\n              <Button size=\"small\" type=\"text\" @click=\"handleDelParam(index)\">\n                {{ t('common.delete') }}\n              </Button>\n            </template>\n            <div class=\"flex grid grid-cols-4 gap-8\">\n              <Button\n                v-for=\"item in componentList\"\n                :key=\"item\"\n                @click=\"onComponentChange(item, index)\"\n              >\n                {{ item }}\n              </Button>\n            </div>\n          </Card>\n        </template>\n      </div>\n\n      <div :class=\"plugin.configuration.length !== 0 ? 'mt-8' : ''\" class=\"mx-8\">\n        <Button type=\"primary\" icon=\"add\" class=\"w-full\" @click=\"handleAddParam\" />\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/PluginsView/components/PluginHub.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useBool } from '@/hooks'\nimport { usePluginsStore } from '@/stores'\nimport { deepClone, message, sleep } from '@/utils'\n\nimport type { Plugin } from '@/types/app'\nconst keywords = ref('')\n\nconst { t } = useI18n()\nconst pluginsStore = usePluginsStore()\n\nconst [tagsVisible, toggleTagsVisible] = useBool(false)\nconst tags = ref<Set<string>>(new Set())\n\nconst allTags = computed(() => {\n  const tagCountMap = new Map()\n\n  for (const plugin of pluginsStore.pluginHub) {\n    for (const tag of plugin.tags) {\n      tagCountMap.set(tag, (tagCountMap.get(tag) || 0) + 1)\n    }\n  }\n\n  return Array.from(tagCountMap, ([name, count]) => ({ name, count }))\n})\n\nconst onTagClose = (tag: string) => tags.value.delete(tag)\n\nconst toggleChecked = (tag: string) => {\n  tags.value.has(tag) ? tags.value.delete(tag) : tags.value.add(tag)\n}\n\nconst filteredPlugins = computed(() => {\n  const allPlugins = pluginsStore.pluginHub\n  const keyword = keywords.value.trim()\n  const selectedTags = tags.value\n\n  if (!keyword && selectedTags.size === 0) return allPlugins\n\n  return allPlugins.filter((plugin) => {\n    const matchesKeyword =\n      !keyword || (plugin.name + plugin.id + plugin.description).includes(keyword)\n\n    const matchesTags =\n      selectedTags.size === 0 || Array.from(selectedTags).every((tag) => plugin.tags.includes(tag))\n\n    return matchesKeyword && matchesTags\n  })\n})\n\nconst handleAddPlugin = async (plugin: Plugin) => {\n  const { success, error, destroy } = message.info('plugins.updating', 60 * 1000)\n  try {\n    await pluginsStore.addPlugin(deepClone(plugin))\n    success('common.success')\n  } catch (err: any) {\n    error(err.message || err)\n  } finally {\n    sleep(1000).then(destroy)\n  }\n}\n\nconst handleUpdatePluginHub = async () => {\n  try {\n    await pluginsStore.updatePluginHub()\n    message.success('plugins.updateSuccess')\n  } catch (err: any) {\n    message.error(err.message || err)\n  }\n}\n\nconst isAlreadyAdded = (id: string) => pluginsStore.getPluginById(id)\n\nif (pluginsStore.pluginHub.length === 0) {\n  pluginsStore.updatePluginHub()\n}\n</script>\n\n<template>\n  <div class=\"h-full\">\n    <div v-if=\"pluginsStore.pluginHubLoading\" class=\"flex items-center justify-center h-full\">\n      <Button type=\"text\" loading />\n    </div>\n    <div v-else class=\"flex flex-col h-full\">\n      <div class=\"flex items-center gap-8\">\n        <Button\n          icon=\"settings3\"\n          size=\"small\"\n          :icon-color=\"tagsVisible ? 'var(--primary-color)' : ''\"\n          @click=\"toggleTagsVisible\"\n        />\n        <Input\n          v-model=\"keywords\"\n          :border=\"false\"\n          :placeholder=\"t('plugins.total') + ': ' + pluginsStore.pluginHub.length\"\n          clearable\n          size=\"small\"\n          class=\"flex-1\"\n        >\n          <template #suffix>\n            <Tag\n              v-for=\"tag in tags\"\n              :key=\"tag\"\n              color=\"cyan\"\n              size=\"small\"\n              closeable\n              @close=\"onTagClose(tag)\"\n              @click=\"onTagClose(tag)\"\n            >\n              {{ tag }}\n            </Tag>\n          </template>\n        </Input>\n        <Button icon=\"refresh\" size=\"small\" @click=\"handleUpdatePluginHub\">\n          {{ t('plugins.update') }}\n        </Button>\n      </div>\n      <div v-if=\"tagsVisible\" class=\"flex flex-wrap gap-2 mt-8\">\n        <Tag\n          v-for=\"tag in allTags\"\n          :key=\"tag.name\"\n          :color=\"tags.has(tag.name) ? 'primary' : 'default'\"\n          class=\"cursor-pointer\"\n          @click=\"toggleChecked(tag.name)\"\n        >\n          {{ `${tag.name}(${tag.count})` }}\n        </Tag>\n      </div>\n\n      <Empty v-if=\"filteredPlugins.length === 0\" />\n\n      <div class=\"overflow-y-auto grid grid-cols-3 text-12 gap-8 mt-8 pb-16 pr-8\">\n        <Card v-for=\"plugin in filteredPlugins\" :key=\"plugin.id\" :title=\"plugin.name\">\n          <div class=\"flex flex-col h-full\">\n            <div v-tips=\"plugin.description\" class=\"flex-1 line-clamp-2\">\n              {{ plugin.description }}\n            </div>\n            <div class=\"flex items-center justify-end\">\n              <Button v-if=\"isAlreadyAdded(plugin.id)\" type=\"text\" size=\"small\">\n                {{ t('common.added') }}\n              </Button>\n              <Button v-else type=\"link\" size=\"small\" @click=\"handleAddPlugin(plugin)\">\n                {{ t('common.add') }}\n              </Button>\n            </div>\n          </div>\n        </Card>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/PluginsView/components/PluginView.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, h } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { ReadFile, WriteFile } from '@/bridge'\nimport { PluginTriggerEvent } from '@/enums/app'\nimport { usePluginsStore } from '@/stores'\nimport { deepClone, ignoredError, message } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\nimport Dropdown from '@/components/Dropdown/index.vue'\n\nimport type { Plugin } from '@/types/app'\n\ninterface Props {\n  id: string\n}\n\nconst props = defineProps<Props>()\n\nconst loading = ref(false)\nconst plugin = ref<Plugin>()\nconst metadata = ref<Record<string, any>>()\nconst code = ref('')\n\nconst { t } = useI18n()\nconst pluginsStore = usePluginsStore()\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst handleSave = async () => {\n  if (!plugin.value) return\n  loading.value = true\n  try {\n    await WriteFile(plugin.value.path, code.value)\n    await pluginsStore.reloadPlugin(plugin.value, code.value, false)\n    handleSubmit()\n  } catch (error: any) {\n    message.error(error)\n    console.log(error)\n  }\n  loading.value = false\n}\n\nconst testing = ref(false)\n\nconst handleTest = async (event: PluginTriggerEvent, arg1?: any, arg2?: any) => {\n  if (!plugin.value || testing.value) return\n  testing.value = true\n  try {\n    const metadata = JSON.stringify({\n      ...pluginsStore.getPluginMetadata(props.id),\n      Mode: 'Dev',\n    })\n    if (event === PluginTriggerEvent.OnSubscribe) {\n      arg1 = '[]'\n      arg2 = '{}'\n    } else if (event === PluginTriggerEvent.OnGenerate) {\n      arg1 = '{}'\n      arg2 = '{}'\n    } else if (event === PluginTriggerEvent.OnConfigure) {\n      arg1 = metadata\n      arg2 = metadata\n    }\n    const fn = new window.AsyncFunction(\n      `const Plugin = ${metadata};\\n${code.value}\\nreturn await ${event}(${arg1}, ${arg2})`,\n    )\n    await fn()\n    message.success('common.success')\n  } catch (error: any) {\n    message.error(error.message || error)\n  }\n  testing.value = false\n}\n\nconst initPluginCode = async (p: Plugin) => {\n  const _code = pluginsStore.getPluginCodefromCache(p.id)\n  if (_code) {\n    code.value = _code\n    return\n  }\n  const content = (await ignoredError(ReadFile, p.path)) || ''\n  code.value = content\n}\n\nconst p = pluginsStore.getPluginById(props.id)\nif (p) {\n  plugin.value = deepClone(p)\n  metadata.value = pluginsStore.getPluginMetadata(props.id)\n  initPluginCode(p)\n}\n\nconst modalSlots = {\n  action: () => {\n    const events = [\n      [PluginTriggerEvent.OnManual, 'plugin.on::manual'],\n      [PluginTriggerEvent.OnInstall, 'plugin.on::install'],\n      [PluginTriggerEvent.OnUninstall, 'plugin.on::uninstall'],\n      [PluginTriggerEvent.OnStartup, 'plugin.on::startup'],\n      [PluginTriggerEvent.OnShutdown, 'plugin.on::shutdown'],\n      [PluginTriggerEvent.OnReady, 'plugin.on::ready'],\n      [PluginTriggerEvent.OnTask, 'plugin.on::task'],\n      [PluginTriggerEvent.OnConfigure, 'plugin.on::configure'],\n      [PluginTriggerEvent.OnSubscribe, 'plugin.on::subscribe'],\n      [PluginTriggerEvent.OnGenerate, 'plugin.on::generate'],\n      [PluginTriggerEvent.OnCoreStarted, 'plugin.on::core::started'],\n      [PluginTriggerEvent.OnCoreStopped, 'plugin.on::core::stopped'],\n      [PluginTriggerEvent.OnBeforeCoreStart, 'plugin.on::before::core::start'],\n      [PluginTriggerEvent.OnBeforeCoreStop, 'plugin.on::before::core::stop'],\n    ] as const\n\n    return h(\n      Dropdown,\n      {\n        placement: 'top',\n        class: 'mr-auto',\n      },\n      {\n        default: () =>\n          h(\n            Button,\n            {\n              loading: testing.value,\n              type: 'link',\n            },\n            () => t('plugins.testRun'),\n          ),\n        overlay: () =>\n          h(\n            'div',\n            {\n              class: 'p-4 flex flex-col gap-2 min-w-128',\n            },\n            events.map(([type, label]) =>\n              h(\n                Button,\n                {\n                  onClick: () => handleTest(type),\n                  type: 'text',\n                  size: 'small',\n                },\n                () => t(label),\n              ),\n            ),\n          ),\n      },\n    )\n  },\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <CodeViewer v-model=\"code\" :plugin=\"metadata\" lang=\"javascript\" editable />\n</template>\n"
  },
  {
    "path": "frontend/src/views/PluginsView/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent } from 'vue'\nimport { useI18n, I18nT } from 'vue-i18n'\n\nimport { OpenURI } from '@/bridge'\nimport { DraggableOptions, ViewOptions } from '@/constant/app'\nimport { PluginTriggerEvent, PluginTrigger, View } from '@/enums/app'\nimport { usePluginsStore, useAppSettingsStore, useEnvStore } from '@/stores'\nimport { debounce, message } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\nimport { useModal } from '@/components/Modal'\n\nimport type { Menu, Plugin } from '@/types/app'\n\nconst PluginChangelog = defineAsyncComponent(() => import('./components/PluginChangelog.vue'))\nconst PluginConfigurator = defineAsyncComponent(() => import('./components/PluginConfigurator.vue'))\nconst PluginForm = defineAsyncComponent(() => import('./components/PluginForm.vue'))\nconst PluginHub = defineAsyncComponent(() => import('./components/PluginHub.vue'))\nconst PluginView = defineAsyncComponent(() => import('./components/PluginView.vue'))\n\nconst menuList: Menu[] = [\n  {\n    label: 'plugins.reload',\n    handler: async (id: string) => {\n      const plugin = pluginsStore.getPluginById(id)\n      try {\n        await pluginsStore.reloadPlugin(plugin!)\n        message.success('common.success')\n      } catch (error: any) {\n        console.log(error)\n        message.error(error)\n      }\n    },\n  },\n  {\n    label: 'common.openFile',\n    handler: async (id: string) => {\n      const plugin = pluginsStore.getPluginById(id)\n      await OpenURI(envStore.env.basePath + '/' + plugin!.path)\n    },\n  },\n]\n\nconst { t } = useI18n()\nconst [Modal, modalApi] = useModal({})\n\nconst envStore = useEnvStore()\nconst pluginsStore = usePluginsStore()\nconst appSettingsStore = useAppSettingsStore()\n\nconst handleImportPlugin = () => {\n  modalApi.setProps({\n    title: 'plugins.hub',\n    height: '90',\n    width: '90',\n    submit: false,\n    maskClosable: true,\n    cancelText: 'common.close',\n  })\n  modalApi.setContent(PluginHub).open()\n}\n\nconst openPluginFormModal = (id?: string) => {\n  modalApi.setProps({ title: id ? 'common.edit' : 'common.add', minWidth: '80' })\n  modalApi.setContent(PluginForm, { id }).open()\n}\n\nconst handleAddPlugin = () => {\n  openPluginFormModal()\n}\n\nconst handleEditPlugin = (id: string) => {\n  openPluginFormModal(id)\n}\n\nconst handleViewChangelog = (id: string) => {\n  modalApi.setProps({\n    title: 'Changelog',\n    cancelText: 'common.close',\n    width: '90',\n    height: '90',\n    submit: false,\n    maskClosable: true,\n  })\n  modalApi.setContent(PluginChangelog, { id }).open()\n}\n\nconst handleUpdatePluginHub = async () => {\n  try {\n    await pluginsStore.updatePluginHub()\n    message.success('plugins.updateSuccess')\n  } catch (error: any) {\n    console.error('handleUpdatePluginHub: ', error)\n    message.error(error)\n  }\n}\n\nconst handleUpdatePlugins = async () => {\n  try {\n    await pluginsStore.updatePlugins()\n    message.success('common.success')\n  } catch (error: any) {\n    console.error('handleUpdatePlugins: ', error)\n    message.error(error)\n  }\n}\n\nconst handleUpdatePlugin = async (s: Plugin) => {\n  try {\n    await pluginsStore.updatePlugin(s.id)\n    message.success('common.success')\n  } catch (error: any) {\n    console.error('handleUpdatePlugin: ', error)\n    message.error(error)\n  }\n}\n\nconst handleDeletePlugin = async (p: Plugin) => {\n  try {\n    await pluginsStore.deletePlugin(p.id)\n  } catch (error: any) {\n    console.error('handleDeletePlugin: ', error)\n    message.error(error)\n  }\n}\n\nconst handleDisablePlugin = async (p: Plugin) => {\n  try {\n    p.disabled = !p.disabled\n    pluginsStore.editPlugin(p.id, p)\n  } catch (error: any) {\n    p.disabled = !p.disabled\n    console.error('handleDisablePlugin: ', error)\n    message.error(error)\n  }\n}\n\nconst handleEditPluginCode = (id: string, title: string) => {\n  modalApi.setProps({ title, width: '90' })\n  modalApi.setContent(PluginView, { id }).open()\n}\n\nconst handleInstallation = async (p: Plugin) => {\n  p.loading = true\n  try {\n    if (p.installed) {\n      await pluginsStore.manualTrigger(p.id, PluginTriggerEvent.OnUninstall)\n    } else {\n      await pluginsStore.manualTrigger(p.id, PluginTriggerEvent.OnInstall)\n    }\n    p.installed = !p.installed\n    await pluginsStore.editPlugin(p.id, p)\n  } catch (error: any) {\n    message.error(error)\n  }\n  p.loading = false\n}\n\nconst handleOnRun = async (p: Plugin) => {\n  p.running = true\n  try {\n    await pluginsStore.manualTrigger(p.id, PluginTriggerEvent.OnManual)\n  } catch (error: any) {\n    message.error(error)\n  }\n  p.running = false\n}\n\nconst generateMenus = (p: Plugin) => {\n  const builtInMenus: Menu[] = menuList.map((v) => ({ ...v, handler: () => v.handler?.(p.id) }))\n\n  if (p.configuration.length) {\n    builtInMenus.push({\n      label: 'plugins.configuration',\n      handler: async () => {\n        modalApi.setProps({ title: 'plugins.configuration' })\n        modalApi.setContent(PluginConfigurator, { plugin: p }).open()\n      },\n    })\n  }\n\n  if (Object.keys(p.menus).length !== 0) {\n    builtInMenus.push({\n      label: '',\n      separator: true,\n    })\n  }\n\n  const pluginMenus: Menu[] = Object.entries(p.menus).map(([title, fn]) => ({\n    label: title,\n    handler: async () => {\n      try {\n        p.running = true\n        await pluginsStore.manualTrigger(p.id, fn as any)\n      } catch (error: any) {\n        message.error(error)\n      } finally {\n        p.running = false\n      }\n    },\n  }))\n\n  return builtInMenus.concat(...pluginMenus)\n}\n\nconst noUpdateNeeded = computed(() => pluginsStore.plugins.every((v) => v.disabled))\n\nconst onSortUpdate = debounce(pluginsStore.savePlugins, 1000)\n</script>\n\n<template>\n  <div v-if=\"pluginsStore.plugins.length === 0\" class=\"grid-list-empty\">\n    <Empty>\n      <template #description>\n        <I18nT keypath=\"plugins.empty\" tag=\"div\" scope=\"global\" class=\"flex items-center mt-12\">\n          <template #action>\n            <Button type=\"link\" @click=\"handleAddPlugin\">{{ t('common.add') }}</Button>\n          </template>\n          <template #import>\n            <Button type=\"link\" @click=\"handleImportPlugin\">{{ t('plugins.hub') }}</Button>\n          </template>\n        </I18nT>\n      </template>\n    </Empty>\n  </div>\n\n  <div v-else class=\"grid-list-header\">\n    <Radio v-model=\"appSettingsStore.app.pluginsView\" :options=\"ViewOptions\" class=\"mr-auto\" />\n    <Button type=\"link\" @click=\"handleImportPlugin\">\n      {{ t('plugins.hub') }}\n    </Button>\n    <Dropdown>\n      <template #default=\"{ close }\">\n        <Button\n          :loading=\"pluginsStore.pluginHubLoading\"\n          type=\"link\"\n          @click=\"\n            () => {\n              handleUpdatePluginHub()\n              close()\n            }\n          \"\n        >\n          {{ t('plugins.checkForUpdates') }}\n        </Button>\n      </template>\n      <template #overlay=\"{ close }\">\n        <div class=\"p-4 min-w-128\">\n          <Button\n            :disabled=\"noUpdateNeeded\"\n            type=\"text\"\n            class=\"w-full\"\n            @click=\"\n              () => {\n                handleUpdatePlugins()\n                close()\n              }\n            \"\n          >\n            {{ t('common.updateAll') }}\n          </Button>\n        </div>\n      </template>\n    </Dropdown>\n    <Button type=\"primary\" icon=\"add\" class=\"ml-16\" @click=\"handleAddPlugin\">\n      {{ t('common.add') }}\n    </Button>\n  </div>\n\n  <div\n    v-draggable=\"[pluginsStore.plugins, { ...DraggableOptions, onUpdate: onSortUpdate }]\"\n    :class=\"'grid-list-' + appSettingsStore.app.pluginsView\"\n  >\n    <Card\n      v-for=\"p in pluginsStore.plugins\"\n      :key=\"p.id\"\n      v-menu=\"generateMenus(p)\"\n      :title=\"p.name\"\n      :disabled=\"p.disabled\"\n      class=\"grid-list-item\"\n    >\n      <template #title-prefix>\n        <Tag v-if=\"pluginsStore.isDevVersion(p)\" color=\"purple\" size=\"small\">Dev</Tag>\n        <Tag v-if=\"pluginsStore.isDeprecated(p)\" color=\"red\" size=\"small\">\n          {{ t('plugins.deprecated') }}\n        </Tag>\n        <Tag\n          v-if=\"pluginsStore.hasNewPluginVersion(p)\"\n          size=\"small\"\n          color=\"cyan\"\n          class=\"cursor-pointer\"\n          @click=\"handleViewChangelog(p.id)\"\n        >\n          {{ t('plugins.newVersion') }}\n        </Tag>\n        <div\n          v-show=\"p.status !== 0\"\n          :style=\"{\n            color: { 1: 'greenyellow', 2: 'red' }[p.status],\n          }\"\n          class=\"pr-4\"\n        >\n          ●\n        </div>\n        <Tag v-if=\"p.updating\" color=\"cyan\" size=\"small\">\n          {{ t('plugins.updating') }}\n        </Tag>\n      </template>\n\n      <template #extra>\n        <Dropdown v-if=\"appSettingsStore.app.pluginsView === View.Grid\">\n          <Button type=\"link\" size=\"small\" icon=\"more\" />\n          <template #overlay>\n            <div class=\"flex flex-col gap-4 min-w-64 p-4\">\n              <Button\n                :loading=\"p.updating\"\n                :disabled=\"p.disabled\"\n                type=\"text\"\n                @click=\"handleUpdatePlugin(p)\"\n              >\n                {{ t('common.update') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleDisablePlugin(p)\">\n                {{ p.disabled ? t('common.enable') : t('common.disable') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleEditPlugin(p.id)\">\n                {{ t('common.develop') }}\n              </Button>\n              <Button v-if=\"!p.install || !p.installed\" type=\"text\" @click=\"handleDeletePlugin(p)\">\n                {{ t('common.delete') }}\n              </Button>\n            </div>\n          </template>\n        </Dropdown>\n\n        <template v-else>\n          <Button\n            :disabled=\"p.disabled\"\n            :loading=\"p.updating\"\n            type=\"text\"\n            size=\"small\"\n            @click=\"handleUpdatePlugin(p)\"\n          >\n            {{ t('common.update') }}\n          </Button>\n          <Button type=\"text\" size=\"small\" @click=\"handleDisablePlugin(p)\">\n            {{ p.disabled ? t('common.enable') : t('common.disable') }}\n          </Button>\n          <Button type=\"text\" size=\"small\" @click=\"handleEditPlugin(p.id)\">\n            {{ t('common.develop') }}\n          </Button>\n          <Button\n            :disabled=\"p.install && p.installed\"\n            type=\"text\"\n            size=\"small\"\n            @click=\"handleDeletePlugin(p)\"\n          >\n            {{ t('common.delete') }}\n          </Button>\n        </template>\n      </template>\n\n      {{ t('plugin.trigger') }}:\n      <Tag v-if=\"p.triggers.length === 0\" size=\"small\" color=\"red\">{{ t('common.none') }}</Tag>\n\n      <template v-if=\"appSettingsStore.app.pluginsView === View.Grid\">\n        <span v-for=\"trigger in p.triggers.slice(0, 2)\" :key=\"trigger\">\n          <Tag size=\"small\">{{ t('plugin.' + trigger) }}</Tag>\n        </span>\n        <Tag\n          v-if=\"p.triggers.length > 2\"\n          v-tips=\"p.triggers.map((v) => t('plugin.' + v)).join('、')\"\n          size=\"small\"\n          color=\"cyan\"\n        >\n          ...\n        </Tag>\n      </template>\n      <template v-else>\n        <span v-for=\"trigger in p.triggers\" :key=\"trigger\">\n          <Tag size=\"small\">{{ t('plugin.' + trigger) }}</Tag>\n        </span>\n      </template>\n\n      <div\n        v-tips=\"p.description\"\n        :class=\"{ 'line-clamp-1': appSettingsStore.app.pluginsView === View.Grid }\"\n      >\n        {{ t('plugin.description') }}\n        :\n        {{ p.description || '--' }}\n      </div>\n\n      <div class=\"flex mt-4\">\n        <Button\n          type=\"link\"\n          size=\"small\"\n          class=\"pl-4\"\n          style=\"margin-left: -8px\"\n          @click=\"handleEditPluginCode(p.id, p.name)\"\n        >\n          {{ t('plugins.source') }}\n        </Button>\n\n        <Button\n          v-if=\"p.install\"\n          :loading=\"p.loading\"\n          type=\"link\"\n          size=\"small\"\n          auto-size\n          @click=\"handleInstallation(p)\"\n        >\n          {{ t(p.installed ? 'common.uninstall' : 'common.install') }}\n        </Button>\n\n        <template v-if=\"p.triggers.includes(PluginTrigger.OnManual)\">\n          <Button\n            v-if=\"!p.disabled && (!p.install || p.installed)\"\n            :loading=\"p.running\"\n            :icon=\"p.hasUI ? 'sparkle' : undefined\"\n            type=\"primary\"\n            size=\"small\"\n            auto-size\n            class=\"ml-auto\"\n            @click=\"handleOnRun(p)\"\n          >\n            {{ t('common.run') }}\n          </Button>\n        </template>\n      </div>\n    </Card>\n  </div>\n\n  <Modal />\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/DnsConfig.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, ref, useTemplateRef } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { DomainStrategyOptions } from '@/constant/kernel'\n\nimport DnsRulesConfig from './DnsRulesConfig.vue'\nimport DnsServersConfig from './DnsServersConfig.vue'\n\ninterface Props {\n  inboundOptions: { label: string; value: string }[]\n  outboundOptions: { label: string; value: string }[]\n  ruleSet: IRuleSet[]\n}\n\ndefineProps<Props>()\n\nconst model = defineModel<IDNS>({ required: true })\n\nconst serversOptions = computed(() =>\n  model.value.servers.map((v) => ({ label: v.tag, value: v.id })),\n)\n\nconst activeKey = ref('common')\nconst rulesConfigRef = useTemplateRef('rulesConfigRef')\nconst serversConfigRef = useTemplateRef('serversConfigRef')\nconst tabs = [\n  { key: 'common', tab: 'kernel.dns.tab.common' },\n  { key: 'servers', tab: 'kernel.dns.tab.servers' },\n  { key: 'rules', tab: 'kernel.dns.tab.rules' },\n]\n\nconst { t } = useI18n()\n\nconst handleAdd = () => {\n  const handlerMap: Record<string, (() => void) | undefined> = {\n    common: () => {},\n    rules: rulesConfigRef.value?.handleAdd,\n    servers: serversConfigRef.value?.handleAdd,\n  }\n  handlerMap[activeKey.value]?.()\n}\n\ndefineExpose({ handleAdd })\n</script>\n\n<template>\n  <Tabs v-model:active-key=\"activeKey\" :items=\"tabs\" tab-position=\"top\">\n    <template #common>\n      <div class=\"form-item\">\n        {{ t('kernel.dns.disable_cache') }}\n        <Switch v-model=\"model.disable_cache\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.dns.disable_expire') }}\n        <Switch v-model=\"model.disable_expire\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.dns.independent_cache') }}\n        <Switch v-model=\"model.independent_cache\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.dns.final') }}\n        <Select v-model=\"model.final\" :options=\"serversOptions\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.dns.strategy') }}\n        <Select v-model=\"model.strategy\" :options=\"DomainStrategyOptions\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.dns.client_subnet') }}\n        <Input v-model=\"model.client_subnet\" editable />\n      </div>\n    </template>\n    <template #servers>\n      <DnsServersConfig\n        ref=\"serversConfigRef\"\n        v-model=\"model.servers\"\n        :outbound-options=\"outboundOptions\"\n        :servers-options=\"serversOptions\"\n      />\n    </template>\n    <template #rules>\n      <DnsRulesConfig\n        ref=\"rulesConfigRef\"\n        v-model=\"model.rules\"\n        :inbound-options=\"inboundOptions\"\n        :outbound-options=\"outboundOptions\"\n        :servers-options=\"serversOptions\"\n        :rule-set=\"ruleSet\"\n      />\n    </template>\n  </Tabs>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/DnsRulesConfig.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { DraggableOptions } from '@/constant/app'\nimport {\n  DnsRuleTypeOptions,\n  DnsRuleActionOptions,\n  DnsRuleActionRejectOptions,\n  DomainStrategyOptions,\n} from '@/constant/kernel'\nimport { DefaultDnsRule } from '@/constant/profile'\nimport {\n  RuleType,\n  ClashMode,\n  RulesetType,\n  RulesetFormat,\n  RuleAction,\n  RuleActionReject,\n  Strategy,\n} from '@/enums/kernel'\nimport { useBool } from '@/hooks'\nimport { deepClone, isValidJson, message } from '@/utils'\n\ninterface Props {\n  inboundOptions: { label: string; value: string }[]\n  outboundOptions: { label: string; value: string }[]\n  serversOptions: { label: string; value: string }[]\n  ruleSet: IRuleSet[]\n}\n\nconst props = defineProps<Props>()\n\nconst model = defineModel<IDNSRule[]>({ required: true })\n\nlet ruleId = 0\nconst fields = ref<IDNSRule>(DefaultDnsRule())\n\nconst isInsertionPointMissing = computed(\n  () => model.value.findIndex((rule) => rule.type === RuleType.InsertionPoint) === -1,\n)\n\nconst { t } = useI18n()\nconst [showEditModal] = useBool(false)\n\nconst handleAdd = () => {\n  ruleId = -1\n  fields.value = DefaultDnsRule()\n  showEditModal.value = true\n}\n\ndefineExpose({ handleAdd })\n\nconst handleAddEnd = () => {\n  if (ruleId !== -1) {\n    model.value[ruleId] = fields.value\n  } else {\n    const index = model.value.findIndex((v) => v.type === RuleType.InsertionPoint)\n    if (index !== -1) {\n      model.value.splice(index + 1, 0, fields.value)\n    } else {\n      model.value.unshift(fields.value)\n    }\n  }\n}\n\nconst handleEdit = (index: number) => {\n  ruleId = index\n  fields.value = deepClone(model.value[index]!)\n  showEditModal.value = true\n}\n\nconst handleAddInsertionPoint = () => {\n  model.value.unshift({\n    id: RuleType.InsertionPoint,\n    type: RuleType.InsertionPoint,\n    enable: true,\n    payload: '',\n    action: RuleAction.Route,\n    server: '',\n    invert: false,\n    strategy: Strategy.Default,\n    disable_cache: false,\n    client_subnet: '',\n  })\n}\n\nconst handleDeleteRule = (index: number) => {\n  model.value.splice(index, 1)\n}\n\nconst handleUse = (ruleset: any) => {\n  const ids = fields.value.payload.split(',').filter((v) => v)\n  const idx = ids.findIndex((v) => v === ruleset.id)\n  if (idx === -1) {\n    ids.push(ruleset.id)\n  } else {\n    ids.splice(idx, 1)\n  }\n  fields.value.payload = ids.join(',')\n}\n\nconst handleClearRuleset = (ruleset: any) => {\n  const ids = fields.value.payload.split(',').filter((id) => props.ruleSet.find((v) => v.id === id))\n  ruleset.payload = ids.join(',')\n}\n\nconst showLost = () => message.warn('kernel.route.rules.invalid')\n\nconst hasLost = (rule: IDNSRule) => {\n  const checkServer = () => {\n    if (rule.action === RuleAction.Route) {\n      if (!props.serversOptions.find((v) => v.value === rule.server)) {\n        return true\n      }\n      return false\n    } else if ([RuleAction.RouteOptions, RuleAction.Predefined].includes(rule.action as any)) {\n      return !isValidJson(rule.server)\n    } else if (rule.action === RuleAction.Reject) {\n      return ![RuleActionReject.Default, RuleActionReject.Drop].includes(rule.server as any)\n    }\n    return false\n  }\n\n  const checkPayload = () => {\n    if (rule.type === RuleType.Inbound) {\n      return !props.inboundOptions.find((v) => v.value === rule.payload)\n    }\n    if (rule.type === RuleType.RuleSet) {\n      const hasMissingRuleset = rule.payload\n        .split(',')\n        .some((id) => !props.ruleSet.find((v) => v.id === id))\n      return hasMissingRuleset\n    }\n    if (rule.type === RuleType.Inline) {\n      return !isValidJson(rule.payload)\n    }\n    return !rule.payload\n  }\n\n  return checkServer() || checkPayload()\n}\n\nconst renderRule = (rule: IDNSRule) => {\n  const { type, payload, server, action, invert } = rule\n  const children: string[] = [type]\n  let _payload = payload\n  if (type === RuleType.RuleSet) {\n    _payload = rule.payload\n      .split(',')\n      .map((id) => props.ruleSet.find((v) => v.id === id)?.tag || id)\n      .join(',')\n  } else if (type === RuleType.Inline && payload.includes('__is_fake_ip')) {\n    _payload = 'FakeIP'\n  }\n  if (invert) {\n    _payload += ` (invert) `\n  }\n  children.push(_payload, action)\n  if (server) {\n    const proxy = props.serversOptions.find((v) => v.value === server)?.label || server\n    children.push(proxy)\n  }\n  return children.join(',')\n}\n</script>\n<template>\n  <Empty v-if=\"model.length === 0 || (model.length === 1 && !isInsertionPointMissing)\">\n    <template #description>\n      <Button icon=\"add\" type=\"primary\" size=\"small\" @click=\"handleAdd\">\n        {{ t('common.add') }}\n      </Button>\n    </template>\n  </Empty>\n\n  <Divider v-if=\"isInsertionPointMissing\">\n    <Button type=\"text\" size=\"small\" @click=\"handleAddInsertionPoint\">\n      {{ t('kernel.addInsertionPoint') }}\n    </Button>\n  </Divider>\n\n  <div v-draggable=\"[model, DraggableOptions]\">\n    <Card v-for=\"(rule, index) in model\" :key=\"rule.id\" class=\"mb-2\">\n      <div v-if=\"rule.type === RuleType.InsertionPoint\" class=\"text-center font-bold\">\n        <Divider class=\"cursor-move\">\n          <Button icon=\"add\" type=\"text\" size=\"small\" @click=\"handleAdd\">\n            {{ t('kernel.insertionPoint') }}\n          </Button>\n        </Divider>\n      </div>\n      <div v-else class=\"flex items-center py-2 gap-8\">\n        <Switch v-model=\"rule.enable\" border=\"square\" size=\"small\" />\n        <div class=\"font-bold\">\n          <span v-if=\"hasLost(rule)\" class=\"warn cursor-pointer\" @click=\"showLost\"> [ ! ] </span>\n          {{ renderRule(rule) }}\n        </div>\n        <div class=\"ml-auto\">\n          <Button\n            v-if=\"rule.type === RuleType.RuleSet && rule.payload && hasLost(rule)\"\n            size=\"small\"\n            type=\"text\"\n            @click=\"handleClearRuleset(rule)\"\n          >\n            {{ t('common.clear') }}\n          </Button>\n          <Button icon=\"edit\" type=\"text\" size=\"small\" @click=\"handleEdit(index)\" />\n          <Button icon=\"delete\" type=\"text\" size=\"small\" @click=\"handleDeleteRule(index)\" />\n        </div>\n      </div>\n    </Card>\n  </div>\n\n  <Modal\n    v-model:open=\"showEditModal\"\n    :on-ok=\"handleAddEnd\"\n    title=\"kernel.dns.tab.rules\"\n    max-width=\"80\"\n    max-height=\"80\"\n  >\n    <div class=\"form-item\">\n      {{ t('kernel.dns.rules.type') }}\n      <Select v-model=\"fields.type\" :options=\"DnsRuleTypeOptions\" />\n    </div>\n    <div class=\"form-item\">\n      {{ t('kernel.dns.rules.action') }}\n      <Radio v-model=\"fields.action\" :options=\"DnsRuleActionOptions\" />\n    </div>\n    <div v-if=\"fields.type !== RuleType.RuleSet\" class=\"form-item\">\n      {{ t('kernel.dns.rules.payload') }}\n      <Radio\n        v-if=\"fields.type === RuleType.ClashMode\"\n        v-model=\"fields.payload\"\n        :options=\"[\n          {\n            label: 'kernel.global',\n            value: ClashMode.Global,\n          },\n          {\n            label: 'kernel.direct',\n            value: ClashMode.Direct,\n          },\n        ]\"\n      />\n      <Select\n        v-else-if=\"fields.type === RuleType.Inbound\"\n        v-model=\"fields.payload\"\n        :options=\"inboundOptions\"\n      />\n      <CodeViewer\n        v-else-if=\"fields.type === RuleType.Inline\"\n        v-model=\"fields.payload\"\n        editable\n        lang=\"json\"\n        style=\"min-width: 320px\"\n      />\n      <Switch\n        v-else-if=\"[RuleType.IpIsPrivate, RuleType.IpAcceptAny].includes(fields.type as any)\"\n        :model-value=\"fields.payload === 'true'\"\n        @change=\"(val) => (fields.payload = val ? 'true' : 'false')\"\n      />\n      <Input v-else v-model=\"fields.payload\" autofocus />\n    </div>\n    <div class=\"form-item\">\n      {{ t('kernel.route.rules.invert') }}\n      <Switch v-model=\"fields.invert\" />\n    </div>\n    <Card class=\"mt-4 mb-16\">\n      <template v-if=\"fields.action === RuleAction.Route\">\n        <div class=\"form-item\">\n          {{ t('kernel.dns.rules.server') }}\n          <Select v-model=\"fields.server\" :options=\"serversOptions\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.strategy') }}\n          <Select v-model=\"fields.strategy\" :options=\"DomainStrategyOptions\" />\n        </div>\n      </template>\n      <template v-else-if=\"fields.action === RuleAction.RouteOptions\">\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.routeOptions') }}\n          <CodeViewer v-model=\"fields.server\" editable lang=\"json\" style=\"min-width: 320px\" />\n        </div>\n      </template>\n      <template v-else-if=\"fields.action === RuleAction.Reject\">\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.action.rejectMethod') }}\n          <Radio v-model=\"fields.server\" :options=\"DnsRuleActionRejectOptions\" />\n        </div>\n      </template>\n      <template v-else-if=\"fields.action === RuleAction.Predefined\">\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.action.predefined') }}\n          <CodeViewer v-model=\"fields.server\" editable lang=\"json\" style=\"min-width: 320px\" />\n        </div>\n      </template>\n      <template v-if=\"[RuleAction.Route, RuleAction.RouteOptions].includes(fields.action as any)\">\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.disable_cache') }}\n          <Switch v-model=\"fields.disable_cache\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.client_subnet') }}\n          <Input v-model=\"fields.client_subnet\" editable />\n        </div>\n      </template>\n    </Card>\n    <template v-if=\"fields.type === RuleType.RuleSet\">\n      <Divider>{{ t('kernel.route.tab.rule_set') }}</Divider>\n      <Empty v-if=\"ruleSet.length === 0\" :description=\"t('kernel.route.rule_set.empty')\" />\n      <div class=\"grid grid-cols-3 gap-8\">\n        <Card\n          v-for=\"ruleset in ruleSet\"\n          :key=\"ruleset.tag\"\n          v-tips=\"ruleset.type\"\n          :title=\"ruleset.tag\"\n          :selected=\"fields.payload.includes(ruleset.id)\"\n          class=\"text-12 line-clamp-1\"\n          @click=\"handleUse(ruleset)\"\n        >\n          {{ ruleset.type }}\n          {{ ruleset.type === RulesetType.Inline ? RulesetFormat.Source : ruleset.format }}\n        </Card>\n      </div>\n    </template>\n  </Modal>\n</template>\n\n<style lang=\"less\" scoped>\n.warn {\n  color: rgb(200, 193, 11);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/DnsServersConfig.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, h, ref, type VNode } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { DraggableOptions } from '@/constant/app'\nimport { DnsServerTypeOptions } from '@/constant/kernel'\nimport { DefaultDnsServer } from '@/constant/profile'\nimport { DnsServer } from '@/enums/kernel'\nimport { useBool } from '@/hooks'\nimport { deepClone, generateDnsServerURL } from '@/utils'\n\nimport Tag from '@/components/Tag/index.vue'\n\ninterface Props {\n  outboundOptions: { label: string; value: string }[]\n  serversOptions: { label: string; value: string }[]\n}\n\nconst props = defineProps<Props>()\n\nconst model = defineModel<IDNSServer[]>({ required: true })\n\nlet serverId = 0\nconst fields = ref<IDNSServer>(DefaultDnsServer())\n\nconst isSupportDetourAndDomainResolver = computed(() => {\n  return [\n    DnsServer.Local,\n    DnsServer.Tcp,\n    DnsServer.Udp,\n    DnsServer.Tls,\n    DnsServer.Quic,\n    DnsServer.Https,\n    DnsServer.H3,\n    DnsServer.Dhcp,\n  ].includes(fields.value.type as any)\n})\n\nconst isSupportServerAndPort = computed(() => {\n  return [\n    DnsServer.Tcp,\n    DnsServer.Udp,\n    DnsServer.Tls,\n    DnsServer.Quic,\n    DnsServer.Https,\n    DnsServer.H3,\n  ].includes(fields.value.type as any)\n})\n\nconst isSupportPath = computed(() =>\n  [DnsServer.Https, DnsServer.H3].includes(fields.value.type as any),\n)\n\nconst { t } = useI18n()\nconst [showEditModal] = useBool(false)\n\nconst handleAdd = () => {\n  serverId = -1\n  fields.value = DefaultDnsServer()\n  showEditModal.value = true\n}\n\ndefineExpose({ handleAdd })\n\nconst handleAddEnd = () => {\n  if (serverId !== -1) {\n    model.value[serverId] = fields.value\n  } else {\n    model.value.unshift(fields.value)\n  }\n}\n\nconst handleEdit = (index: number) => {\n  serverId = index\n  fields.value = deepClone(model.value[index]!)\n  showEditModal.value = true\n}\n\nconst handleDeleteRule = (index: number) => {\n  model.value.splice(index, 1)\n}\n\nconst renderServer = (server: IDNSServer) => {\n  const { tag, detour } = server\n  const children: VNode[] = [\n    h(Tag, { color: 'cyan' }, () => tag),\n    h(Tag, () => generateDnsServerURL(server)),\n  ]\n  if (detour) {\n    const tag = props.outboundOptions.find((v) => v.value === detour)?.label || detour\n    children.push(h(Tag, { color: 'default' }, () => tag))\n  }\n  return h('div', { class: 'font-bold' }, children)\n}\n</script>\n<template>\n  <Empty v-if=\"model.length === 0\">\n    <template #description>\n      <Button icon=\"add\" type=\"primary\" size=\"small\" @click=\"handleAdd\">\n        {{ t('common.add') }}\n      </Button>\n    </template>\n  </Empty>\n\n  <div v-draggable=\"[model, DraggableOptions]\">\n    <Card v-for=\"(server, index) in model\" :key=\"server.id\" class=\"mb-2\">\n      <div class=\"flex items-center py-2\">\n        <component :is=\"renderServer(server)\" />\n        <div class=\"ml-auto\">\n          <Button icon=\"edit\" type=\"text\" size=\"small\" @click=\"handleEdit(index)\" />\n          <Button icon=\"delete\" type=\"text\" size=\"small\" @click=\"handleDeleteRule(index)\" />\n        </div>\n      </div>\n    </Card>\n  </div>\n\n  <Modal\n    v-model:open=\"showEditModal\"\n    :on-ok=\"handleAddEnd\"\n    title=\"kernel.dns.tab.servers\"\n    max-width=\"80\"\n    max-height=\"80\"\n  >\n    <div class=\"flex flex-col\">\n      <div class=\"form-item\">\n        {{ t('kernel.dns.type.name') }}\n        <Select v-model=\"fields.type\" :options=\"DnsServerTypeOptions\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.dns.tag') }}\n        <Input v-model=\"fields.tag\" autofocus />\n      </div>\n      <template v-if=\"isSupportDetourAndDomainResolver\">\n        <div class=\"form-item\">\n          {{ t('kernel.dns.domain_resolver') }}\n          <Select v-model=\"fields.domain_resolver\" :options=\"serversOptions\" clearable />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.dns.detour') }}\n          <Select v-model=\"fields.detour\" :options=\"outboundOptions\" clearable />\n        </div>\n        <template v-if=\"isSupportServerAndPort\">\n          <div class=\"form-item\">\n            {{ t('kernel.dns.server') }}\n            <Input v-model=\"fields.server\" placeholder=\"192.168.1.1,223.5.5.5\" />\n          </div>\n          <div class=\"form-item\">\n            {{ t('kernel.dns.server_port') }}\n            <Input v-model=\"fields.server_port\" placeholder=\"53,853,443,784\" />\n          </div>\n          <div v-if=\"isSupportPath\" class=\"form-item\">\n            {{ t('kernel.dns.path') }}\n            <Input v-model=\"fields.path\" placeholder=\"/dns-query\" />\n          </div>\n        </template>\n      </template>\n      <template v-if=\"fields.type === DnsServer.Hosts\">\n        <div :class=\"{ 'items-start': fields.hosts_path.length !== 0 }\" class=\"form-item\">\n          {{ t('kernel.dns.hosts_path') }}\n          <InputList v-model=\"fields.hosts_path\" placeholder=\"/etc/hosts,c:\\...\\hosts\" />\n        </div>\n        <div\n          :class=\"{ 'items-start': Object.keys(fields.predefined).length !== 0 }\"\n          class=\"form-item\"\n        >\n          {{ t('kernel.dns.predefined') }}\n          <KeyValueEditor\n            v-model=\"fields.predefined\"\n            :placeholder=\"['google.com', '127.0.0.1,::1']\"\n          />\n        </div>\n      </template>\n      <div v-else-if=\"fields.type === DnsServer.Dhcp\" class=\"form-item\">\n        {{ t('kernel.dns.interface') }}\n        <Input v-model=\"fields.interface\" placeholder=\"wlan0,eth0\" />\n      </div>\n      <template v-else-if=\"fields.type === DnsServer.FakeIP\">\n        <div class=\"form-item\">\n          {{ t('kernel.dns.inet4_range') }}\n          <Input v-model=\"fields.inet4_range\" placeholder=\"198.18.0.0/15\" clearable>\n            <template #suffix>\n              <Button\n                size=\"small\"\n                type=\"text\"\n                icon=\"reset\"\n                @click=\"fields.inet4_range = '198.18.0.0/15'\"\n              />\n            </template>\n          </Input>\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.dns.inet6_range') }}\n          <Input v-model=\"fields.inet6_range\" placeholder=\"fc00::/18\" clearable>\n            <template #suffix>\n              <Button\n                size=\"small\"\n                type=\"text\"\n                icon=\"reset\"\n                @click=\"fields.inet6_range = 'fc00::/18'\"\n              />\n            </template>\n          </Input>\n        </div>\n      </template>\n    </div>\n  </Modal>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/GeneralConfig.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useI18n } from 'vue-i18n'\n\nimport { ModeOptions, LogLevelOptions } from '@/constant/kernel'\nimport { useBool } from '@/hooks'\nimport { generateSecureKey } from '@/utils'\n\ninterface Props {\n  outboundOptions: { label: string; value: string }[]\n}\n\ndefineProps<Props>()\n\nconst model = defineModel<{ log: IProfile['log']; experimental: IProfile['experimental'] }>({\n  required: true,\n})\n\nconst { t } = useI18n()\nconst [showMore, toggleMore] = useBool(false)\n</script>\n\n<template>\n  <div>\n    <div class=\"form-item\">\n      {{ t('kernel.clash_api.default_mode') }}\n      <Radio v-model=\"model.experimental.clash_api.default_mode\" :options=\"ModeOptions\" />\n    </div>\n    <div class=\"form-item\">\n      {{ t('kernel.log.disabled') }}\n      <Switch v-model=\"model.log.disabled\" />\n    </div>\n    <template v-if=\"!model.log.disabled\">\n      <div class=\"form-item\">\n        {{ t('kernel.log.level') }}\n        <Radio v-model=\"model.log.level\" :options=\"LogLevelOptions\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.log.output') }}\n        <Input v-model=\"model.log.output\" editable />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.log.timestamp') }}\n        <Switch v-model=\"model.log.timestamp\" />\n      </div>\n    </template>\n    <div class=\"form-item\">\n      {{ t('kernel.clash_api.external_controller') }}\n      <Input v-model=\"model.experimental.clash_api.external_controller\" editable />\n    </div>\n    <div class=\"form-item\">\n      {{ t('kernel.clash_api.secret') }}\n      <div class=\"flex items-center\">\n        <Input v-model=\"model.experimental.clash_api.secret\" editable>\n          <template #suffix>\n            <Button\n              type=\"text\"\n              size=\"small\"\n              icon=\"refresh\"\n              @click=\"() => (model.experimental.clash_api.secret = generateSecureKey())\"\n            />\n          </template>\n        </Input>\n      </div>\n    </div>\n    <Divider>\n      <Button type=\"text\" size=\"small\" @click=\"toggleMore\">{{ t('common.more') }}</Button>\n    </Divider>\n    <div v-show=\"showMore\">\n      <div class=\"form-item\">\n        {{ t('kernel.clash_api.external_ui') }}\n        <Input v-model=\"model.experimental.clash_api.external_ui\" editable />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.clash_api.external_ui_download_url') }}\n        <Input v-model=\"model.experimental.clash_api.external_ui_download_url\" editable />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.clash_api.external_ui_download_detour') }}\n        <Select\n          v-model=\"model.experimental.clash_api.external_ui_download_detour\"\n          :options=\"outboundOptions\"\n          clearable\n        />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.clash_api.access_control_allow_private_network') }}\n        <Switch v-model=\"model.experimental.clash_api.access_control_allow_private_network\" />\n      </div>\n      <div\n        :class=\"{\n          'items-start': model.experimental.clash_api.access_control_allow_origin.length !== 0,\n        }\"\n        class=\"form-item\"\n      >\n        {{ t('kernel.clash_api.access_control_allow_origin') }}\n        <InputList v-model=\"model.experimental.clash_api.access_control_allow_origin\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.cache_file.enabled') }}\n        <Switch v-model=\"model.experimental.cache_file.enabled\" />\n      </div>\n      <template v-if=\"model.experimental.cache_file.enabled\">\n        <div class=\"form-item\">\n          {{ t('kernel.cache_file.path') }}\n          <Input v-model=\"model.experimental.cache_file.path\" editable />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.cache_file.cache_id') }}\n          <Input v-model=\"model.experimental.cache_file.cache_id\" editable />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.cache_file.store_fakeip') }}\n          <Switch v-model=\"model.experimental.cache_file.store_fakeip\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.cache_file.store_rdrc') }}\n          <Switch v-model=\"model.experimental.cache_file.store_rdrc\" />\n        </div>\n      </template>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/InboundsConfig.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useI18n } from 'vue-i18n'\n\nimport { DraggableOptions } from '@/constant/app'\nimport { TunStackOptions } from '@/constant/kernel'\nimport {\n  DefaultInboundMixed,\n  DefaultInboundHttp,\n  DefaultInboundSocks,\n  DefaultInboundTun,\n} from '@/constant/profile'\nimport { Inbound } from '@/enums/kernel'\nimport { picker, sampleID } from '@/utils'\n\nconst model = defineModel<IProfile['inbounds']>({ required: true })\n\nconst { t } = useI18n()\n\nconst handleDelete = (index: number) => {\n  model.value.splice(index, 1)\n}\n\nconst inbounds = [\n  {\n    label: 'Mixed',\n    value: () => {\n      model.value.push({\n        id: sampleID(),\n        tag: 'mixed-in',\n        type: Inbound.Mixed,\n        enable: true,\n        mixed: DefaultInboundMixed(),\n      })\n    },\n  },\n  {\n    label: 'Http',\n    value: () => {\n      model.value.push({\n        id: sampleID(),\n        tag: 'http-in',\n        type: Inbound.Http,\n        enable: true,\n        http: DefaultInboundHttp(),\n      })\n    },\n  },\n  {\n    label: 'Socks',\n    value: () => {\n      model.value.push({\n        id: sampleID(),\n        tag: 'socks-in',\n        type: Inbound.Socks,\n        enable: true,\n        socks: DefaultInboundSocks(),\n      })\n    },\n  },\n  {\n    label: 'Tun',\n    value: () => {\n      model.value.push({\n        id: sampleID(),\n        tag: 'tun-in',\n        type: Inbound.Tun,\n        enable: true,\n        tun: DefaultInboundTun(),\n      })\n    },\n  },\n]\n\nconst handleAdd = async () => {\n  const fns = await picker.multi('common.add', inbounds)\n  fns.forEach((fn) => fn())\n}\n\ndefineExpose({ handleAdd })\n</script>\n\n<template>\n  <Empty v-if=\"model.length === 0\">\n    <template #description>\n      <div class=\"flex gap-8\">\n        <Button v-for=\"inbound in inbounds\" :key=\"inbound.label\" @click=\"inbound.value\">\n          {{ t('common.add') }} {{ inbound.label }}\n        </Button>\n      </div>\n    </template>\n  </Empty>\n  <div v-draggable=\"[model, { ...DraggableOptions, handle: '.drag' }]\">\n    <Card v-for=\"(inbound, index) in model\" :key=\"inbound.id\" :title=\"inbound.tag\" class=\"mb-8\">\n      <template #title-prefix>\n        <Icon icon=\"drag\" class=\"drag cursor-move\" />\n      </template>\n      <template #extra>\n        <Button icon=\"delete\" type=\"text\" size=\"small\" @click=\"handleDelete(index)\" />\n      </template>\n      <div class=\"form-item\">\n        {{ t('kernel.inbounds.enable') }}\n        <Switch v-model=\"inbound.enable\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.inbounds.tag') }}\n        <Input v-model=\"inbound.tag\" />\n      </div>\n      <div v-if=\"inbound.type !== Inbound.Tun && inbound[inbound.type]\">\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.listen.listen') }}\n          <Input v-model=\"inbound[inbound.type]!.listen.listen\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.listen.listen_port') }}\n          <Input v-model=\"inbound[inbound.type]!.listen.listen_port\" type=\"number\" />\n        </div>\n        <div :class=\"{ 'items-start': inbound[inbound.type]!.users.length }\" class=\"form-item\">\n          {{ t('kernel.inbounds.users') }}\n          <InputList v-model=\"inbound[inbound.type]!.users\" placeholder=\"user:password\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.listen.tcp_fast_open') }}\n          <Switch v-model=\"inbound[inbound.type]!.listen.tcp_fast_open\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.listen.tcp_multi_path') }}\n          <Switch v-model=\"inbound[inbound.type]!.listen.tcp_multi_path\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.listen.udp_fragment') }}\n          <Switch v-model=\"inbound[inbound.type]!.listen.udp_fragment\" />\n        </div>\n      </div>\n      <div v-else-if=\"inbound.type === Inbound.Tun && inbound.tun\">\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.tun.interface_name') }}\n          <Input v-model=\"inbound.tun.interface_name\" editable />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.tun.stack') }}\n          <Radio v-model=\"inbound.tun.stack\" :options=\"TunStackOptions\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.tun.auto_route') }}\n          <Switch v-model=\"inbound.tun.auto_route\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.tun.strict_route') }}\n          <Switch v-model=\"inbound.tun.strict_route\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.tun.endpoint_independent_nat') }}\n          <Switch v-model=\"inbound.tun.endpoint_independent_nat\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.inbounds.tun.mtu') }}\n          <Input v-model=\"inbound.tun.mtu\" type=\"number\" editable />\n        </div>\n        <div :class=\"{ 'items-start': inbound.tun.address.length }\" class=\"form-item\">\n          {{ t('kernel.inbounds.tun.address') }}\n          <InputList v-model=\"inbound.tun.address\" />\n        </div>\n        <div :class=\"{ 'items-start': inbound.tun.route_address.length }\" class=\"form-item\">\n          {{ t('kernel.inbounds.tun.route_address') }}\n          <InputList v-model=\"inbound.tun.route_address\" placeholder=\"0.0.0.0/1 ::1\" />\n        </div>\n        <div :class=\"{ 'items-start': inbound.tun.route_exclude_address.length }\" class=\"form-item\">\n          {{ t('kernel.inbounds.tun.route_exclude_address') }}\n          <InputList\n            v-model=\"inbound.tun.route_exclude_address\"\n            placeholder=\"192.168.0.0/16 fc00::/7\"\n          />\n        </div>\n      </div>\n    </Card>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/MixinAndScriptConfig.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { parse, stringify } from 'yaml'\n\nimport { message } from '@/utils'\n\nconst model = defineModel<{ mixin: IProfile['mixin']; script: IProfile['script'] }>({\n  required: true,\n})\n\nconst { t } = useI18n()\n\nconst activeTab = ref('mixin')\n\nconst tabItems = [\n  { key: 'mixin', tab: 'profile.mixinSettings.name' },\n  { key: 'script', tab: 'profile.scriptSettings.name' },\n]\n\nconst MixinPriorityOptions = [\n  { label: 'profile.mixinSettings.mixin', value: 'mixin' },\n  { label: 'profile.mixinSettings.gui', value: 'gui' },\n]\n\nconst MixinFormatOptions = [\n  { label: 'JSON', value: 'json' },\n  { label: 'YAML', value: 'yaml' },\n]\n\nconst onFormatChange = (val: 'json' | 'yaml', old: 'json' | 'yaml') => {\n  try {\n    const config = parse(model.value.mixin.config)\n    if (config) {\n      if (val === 'json') {\n        model.value.mixin.config = JSON.stringify(config, null, 2)\n      } else {\n        model.value.mixin.config = stringify(config)\n      }\n    }\n  } catch (e: any) {\n    model.value.mixin.format = old\n    message.error(e.message || e)\n  }\n}\n</script>\n\n<template>\n  <Tabs v-model:active-key=\"activeTab\" :items=\"tabItems\">\n    <template #mixin>\n      <div class=\"form-item\">\n        {{ t('profile.mixinSettings.priority') }}\n        <Radio v-model=\"model.mixin.priority\" :options=\"MixinPriorityOptions\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('profile.mixinSettings.format') }}\n        <Radio\n          v-model=\"model.mixin.format\"\n          :options=\"MixinFormatOptions\"\n          @change=\"onFormatChange\"\n        />\n      </div>\n      <CodeViewer v-model=\"model.mixin.config\" :lang=\"model.mixin.format\" editable />\n    </template>\n    <template #script>\n      <CodeViewer v-model=\"model.script.code\" lang=\"javascript\" editable />\n    </template>\n  </Tabs>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/OutboundsConfig.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { DraggableOptions } from '@/constant/app'\nimport { OutboundOptions, BuiltInOutbound } from '@/constant/kernel'\nimport { DefaultOutbound } from '@/constant/profile'\nimport { Outbound } from '@/enums/kernel'\nimport { useSubscribesStore } from '@/stores'\nimport { deepClone, message } from '@/utils'\n\nconst model = defineModel<IProfile['outbounds']>({ required: true })\n\nlet updateGroupId = 0\nconst showEditModal = ref(false)\nconst showSortModal = ref(false)\nconst expandedSet = ref<Set<string>>(new Set(['Built-in', 'Subscription']))\nconst SubscribesNameMap = ref<Record<string, string>>({})\n\nconst proxyGroup = ref([\n  {\n    id: 'Built-in',\n    name: 'kernel.outbounds.builtIn',\n    proxies: [\n      ...BuiltInOutbound.map((v) => ({ id: v, tag: v, type: 'Built-In' })),\n      ...model.value.map(({ id, tag, type }) => ({ id, tag, type: type as string })),\n    ],\n  },\n  {\n    id: 'Subscription',\n    name: 'kernel.outbounds.subscriptions',\n    proxies: [],\n  },\n])\n\nconst fields = ref<IOutbound>(DefaultOutbound())\n\nconst { t } = useI18n()\nconst subscribesStore = useSubscribesStore()\n\nconst handleAdd = () => {\n  updateGroupId = -1\n  fields.value = DefaultOutbound()\n  showEditModal.value = true\n}\n\ndefineExpose({ handleAdd })\n\nconst handleDeleteGroup = (index: number) => {\n  const id = model.value[index]!.id\n  model.value.splice(index, 1)\n  proxyGroup.value = proxyGroup.value.map((v) => ({\n    ...v,\n    proxies: v.proxies.filter((v) => v.id !== id),\n  }))\n}\n\nconst handleClearGroup = async (outbound: IOutbound) => {\n  const filtered = outbound.outbounds.filter(({ id, type }) => {\n    if (type === 'Built-in') {\n      return model.value.some((v) => v.id === id)\n    } else if (type === 'Subscription') {\n      return subscribesStore.getSubscribeById(id)\n    }\n    const sub = subscribesStore.getSubscribeById(type)\n    return sub && sub.proxies.some((v) => v.id === id)\n  })\n  outbound.outbounds.splice(0)\n  outbound.outbounds.push(...filtered)\n}\n\nconst handleAddEnd = () => {\n  const { id, tag, type } = fields.value\n  // Add\n  if (updateGroupId === -1) {\n    model.value.unshift(fields.value)\n    proxyGroup.value[0]!.proxies.unshift({ id, tag, type })\n    return\n  }\n  // Update\n  model.value[updateGroupId] = fields.value\n  const idx = proxyGroup.value[0]!.proxies.findIndex((v) => v.id === id)\n  if (idx !== -1) {\n    proxyGroup.value[0]!.proxies.splice(idx, 1, { id, tag, type })\n    model.value\n      .filter((outbound) => [Outbound.Selector, Outbound.Urltest].includes(outbound.type as any))\n      .forEach(({ outbounds }) => {\n        const proxy = outbounds.find((v) => v.id === id)\n        proxy && (proxy.tag = tag)\n      })\n  }\n}\n\nconst handleEditGroup = (index: number) => {\n  updateGroupId = index\n  fields.value = deepClone(model.value[index]!)\n  showEditModal.value = true\n}\n\nconst handleAddProxy = (groupID: string, proxyID: string, proxyName: string) => {\n  // self\n  if (groupID === 'Built-in' && proxyID === fields.value.id) return\n\n  const idx = fields.value.outbounds.findIndex((outbound) => outbound.id === proxyID)\n  if (idx !== -1) {\n    fields.value.outbounds.splice(idx, 1)\n  } else {\n    fields.value.outbounds.push({ id: proxyID, tag: proxyName, type: groupID })\n  }\n}\n\nconst isInuse = (groupID: string, proxyID: string) => {\n  return fields.value.outbounds.find((outbound) => outbound.id === proxyID)\n}\n\nconst hasLost = (outbound: IOutbound) => {\n  if ([Outbound.Selector, Outbound.Urltest].includes(outbound.type as any)) {\n    return outbound.outbounds.some(({ id, type }) => {\n      if (type === 'Built-in') {\n        if (BuiltInOutbound.includes(id as Outbound)) {\n          return false\n        }\n        return model.value.every((v) => v.id !== id)\n      } else if (type === 'Subscription') {\n        const sub = subscribesStore.getSubscribeById(id)\n        if (!sub) return true\n        return false\n      }\n      const sub = subscribesStore.getSubscribeById(type)\n      if (!sub) return true\n      return sub.proxies.every((v) => v.id !== id)\n    })\n  }\n  return false\n}\n\nconst handleSortGroup = (index: number) => {\n  updateGroupId = index\n  fields.value = deepClone(model.value[index]!)\n  showSortModal.value = true\n}\n\nconst handleSortGroupEnd = () => {\n  model.value[updateGroupId] = fields.value\n}\n\nconst clacSubscriptionsCount = (outbound: IOutbound) => {\n  if ([Outbound.Selector, Outbound.Urltest].includes(outbound.type as any)) {\n    return outbound.outbounds.filter((v) => v.type === 'Subscription').length\n  }\n  return 0\n}\n\nconst clacOutboundsCount = (outbound: IOutbound) => {\n  if ([Outbound.Selector, Outbound.Urltest].includes(outbound.type as any)) {\n    return outbound.outbounds.filter((v) => v.type !== 'Subscription').length\n  }\n  return 0\n}\n\nconst needToAdd = (outbound: IOutbound) => {\n  if ([Outbound.Selector, Outbound.Urltest].includes(outbound.type as any)) {\n    return outbound.outbounds.length === 0\n  }\n  return false\n}\n\nconst toggleExpanded = (key: string) => {\n  if (expandedSet.value.has(key)) {\n    expandedSet.value.delete(key)\n  } else {\n    expandedSet.value.add(key)\n  }\n}\n\nconst isExpanded = (key: string) => expandedSet.value.has(key)\n\nconst showLost = () => message.warn('kernel.outbounds.notFound')\n\nconst showNeedToAdd = () => message.error('kernel.outbounds.needToAdd')\n\nsubscribesStore.subscribes.forEach(async ({ id, name, proxies }) => {\n  proxyGroup.value[1]!.proxies.push({ id, tag: name, type: 'Subscribe' })\n  proxyGroup.value.push({ id, name, proxies })\n  SubscribesNameMap.value[id] = name\n})\n</script>\n\n<template>\n  <Empty v-if=\"model.length === 0\">\n    <template #description>\n      <Button icon=\"add\" type=\"primary\" size=\"small\" @click=\"handleAdd\">\n        {{ t('common.add') }}\n      </Button>\n    </template>\n  </Empty>\n\n  <div v-draggable=\"[model, DraggableOptions]\">\n    <Card v-for=\"(outbound, index) in model\" :key=\"outbound.id\" class=\"mb-2\">\n      <div class=\"flex items-center py-2\">\n        <div class=\"font-bold\" style=\"min-width: 90px\">\n          <span\n            v-if=\"hasLost(outbound)\"\n            class=\"cursor-pointer\"\n            style=\"color: rgb(200, 193, 11)\"\n            @click=\"showLost\"\n          >\n            [ ! ]\n          </span>\n          <span\n            v-if=\"needToAdd(outbound)\"\n            class=\"cursor-pointer\"\n            style=\"color: red\"\n            @click=\"showNeedToAdd\"\n          >\n            [ ! ]\n          </span>\n          {{ outbound.tag }}\n        </div>\n        <Button type=\"link\" size=\"small\" @click=\"handleSortGroup(index)\">\n          (\n          {{ t('kernel.outbounds.refsOutbound') }}:{{ clacOutboundsCount(outbound) }}\n          /\n          {{ t('kernel.outbounds.refsSubscription') }}:{{ clacSubscriptionsCount(outbound) }}\n          )\n        </Button>\n        <div class=\"ml-auto\">\n          <Button v-if=\"hasLost(outbound)\" type=\"text\" @click=\"handleClearGroup(outbound)\">\n            {{ t('common.clear') }}\n          </Button>\n          <Button icon=\"edit\" type=\"text\" size=\"small\" @click=\"handleEditGroup(index)\" />\n          <Button icon=\"delete\" type=\"text\" size=\"small\" @click=\"handleDeleteGroup(index)\" />\n        </div>\n      </div>\n    </Card>\n  </div>\n\n  <Modal\n    v-model:open=\"showSortModal\"\n    :on-ok=\"handleSortGroupEnd\"\n    mask-closable\n    title=\"kernel.outbounds.sort\"\n    max-width=\"80\"\n    max-height=\"80\"\n  >\n    <Divider>{{ t('kernel.outbounds.refs') }}</Divider>\n    <Empty v-if=\"fields.outbounds.length === 0\" />\n    <div v-draggable=\"[fields.outbounds, DraggableOptions]\">\n      <Button v-for=\"proxy in fields.outbounds\" :key=\"proxy.id\" type=\"link\">\n        {{ proxy.tag }}\n      </Button>\n    </div>\n  </Modal>\n\n  <Modal\n    v-model:open=\"showEditModal\"\n    :on-ok=\"handleAddEnd\"\n    title=\"kernel.outbounds.name\"\n    width=\"80\"\n    height=\"80\"\n  >\n    <div class=\"form-item\">\n      {{ t('kernel.outbounds.tag') }}\n      <Input v-model=\"fields.tag\" autofocus />\n    </div>\n    <div class=\"form-item\">\n      {{ t('kernel.outbounds.type') }}\n      <Radio v-model=\"fields.type\" :options=\"OutboundOptions\" />\n    </div>\n    <template v-if=\"Outbound.Selector === fields.type || Outbound.Urltest === fields.type\">\n      <div class=\"form-item\">\n        {{ t('kernel.outbounds.interrupt_exist_connections') }}\n        <Switch v-model=\"fields.interrupt_exist_connections\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.outbounds.include') }}\n        <Input v-model=\"fields.include\" placeholder=\"keywords1|keywords2\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.outbounds.exclude') }}\n        <Input v-model=\"fields.exclude\" placeholder=\"keywords1|keywords2\" />\n      </div>\n    </template>\n    <template v-if=\"Outbound.Direct === fields.type || Outbound.Block === fields.type\">\n      <Empty :description=\"t('kernel.outbounds.directDesc')\" />\n    </template>\n    <template v-else-if=\"fields.type === Outbound.Urltest\">\n      <div class=\"form-item\">\n        {{ t('kernel.outbounds.url') }}\n        <Input v-model=\"fields.url\" placeholder=\"http(s)://\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.outbounds.interval') }}\n        <Input v-model=\"fields.interval\" placeholder=\"3m\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.outbounds.tolerance') }}\n        <Input v-model=\"fields.tolerance\" type=\"number\" />\n      </div>\n    </template>\n    <template v-if=\"[Outbound.Selector, Outbound.Urltest].includes(fields.type as any)\">\n      <Divider>\n        {{ t('kernel.outbounds.refsOutbound') }} & {{ t('kernel.outbounds.refsSubscription') }}\n      </Divider>\n\n      <div v-for=\"group in proxyGroup\" :key=\"group.id\" class=\"group\">\n        <Button\n          :type=\"isExpanded(group.id) ? 'link' : 'text'\"\n          class=\"sticky top-0 backdrop-blur-sm w-full\"\n          @click=\"toggleExpanded(group.id)\"\n        >\n          {{ t(group.name) }}\n          <div class=\"ml-auto mr-8\">{{ group.proxies.length }}</div>\n          <Icon\n            :class=\"{ 'rotate-z': isExpanded(group.id) }\"\n            icon=\"arrowRight\"\n            class=\"action-expand\"\n          />\n        </Button>\n        <div v-show=\"isExpanded(group.id)\">\n          <Empty\n            v-if=\"group.proxies.length === 0\"\n            :description=\"\n              group.id === 'Subscription'\n                ? t('kernel.outbounds.noSubs')\n                : t('kernel.outbounds.empty')\n            \"\n          />\n          <template v-else>\n            <div class=\"w-full grid grid-cols-4 gap-8 p-8\">\n              <Button\n                v-for=\"proxy in group.proxies\"\n                :key=\"proxy.id\"\n                :type=\"isInuse(group.id, proxy.id) ? 'link' : 'text'\"\n                @click=\"handleAddProxy(group.id, proxy.id, proxy.tag)\"\n              >\n                {{ proxy.tag }}\n                <br />\n                {{ proxy.type }}\n              </Button>\n            </div>\n          </template>\n        </div>\n      </div>\n    </template>\n  </Modal>\n</template>\n\n<style lang=\"less\" scoped>\n.action-expand {\n  transition: all 0.2s;\n}\n.rotate-z {\n  transform: rotateZ(90deg);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/ProfileEditor.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, h, onMounted } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { generateConfig, message, restoreProfile } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\nimport { useProfilesStore } from '@/stores'\nimport { Outbound } from '@/enums/kernel'\n\ninterface Props {\n  profile: IProfile\n}\n\nconst props = defineProps<Props>()\n\nconst loading = ref(false)\nconst profileText = ref('')\n\nconst { t } = useI18n()\nconst profilesStore = useProfilesStore()\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst handleSave = async () => {\n  loading.value = true\n  try {\n    const outboundsIds = props.profile.outbounds.reduce((p, c) => {\n      if ([Outbound.Selector, Outbound.Urltest].includes(c.type as any)) {\n        c.outbounds.forEach((o) => {\n          p[o.tag] = o.id\n        })\n      }\n      return p\n    }, {} as Recordable)\n    const newProfile = restoreProfile(JSON.parse(profileText.value), props.profile.name, {\n      extraOutboundsIds: outboundsIds,\n    })\n    newProfile.id = props.profile.id\n    newProfile.mixin = props.profile.mixin\n    newProfile.script = props.profile.script\n    await profilesStore.editProfile(props.profile.id, newProfile)\n    await handleSubmit()\n  } catch (error: any) {\n    console.log(error)\n    message.error(error.message || error)\n  }\n  loading.value = false\n}\n\nonMounted(() => {\n  generateConfig(props.profile, {\n    enableStableConfigCompat: false,\n    enablePluginProcessing: false,\n    enableMixinProcessing: false,\n    enableScriptProcessing: false,\n  }).then((text) => {\n    profileText.value = JSON.stringify(text, null, 2)\n  })\n})\n\nconst modalSlots = {\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <CodeViewer v-model=\"profileText\" lang=\"json\" editable class=\"h-full\" />\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/ProfileForm.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, computed, useTemplateRef, type Ref, h, defineAsyncComponent } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useProfilesStore } from '@/stores'\nimport { deepClone, generateConfig, message, alert } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\nimport Dropdown from '@/components/Dropdown/index.vue'\n\ninterface Props {\n  id?: string\n  step?: number\n}\n\nenum Step {\n  Name = 0,\n  General = 1,\n  Inbounds = 2,\n  Outbounds = 3,\n  Route = 4,\n  Dns = 5,\n  MixinScript = 6,\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  id: '',\n  isUpdate: false,\n  step: Step.Name,\n})\n\nconst DnsConfig = defineAsyncComponent(() => import('./DnsConfig.vue'))\nconst GeneralConfig = defineAsyncComponent(() => import('./GeneralConfig.vue'))\nconst InboundsConfig = defineAsyncComponent(() => import('./InboundsConfig.vue'))\nconst MixinAndScript = defineAsyncComponent(() => import('./MixinAndScriptConfig.vue'))\nconst OutboundsConfig = defineAsyncComponent(() => import('./OutboundsConfig.vue'))\nconst RouteConfig = defineAsyncComponent(() => import('./RouteConfig.vue'))\n\nconst { t } = useI18n()\nconst inboundsRef = useTemplateRef('inboundsRef')\nconst outboundsRef = useTemplateRef('outboundsRef')\nconst routeRef = useTemplateRef('routeRef')\nconst dnsRef = useTemplateRef('dnsRef')\nconst profilesStore = useProfilesStore()\n\nconst loading = ref(false)\nconst currentStep = ref(props.step)\n\nconst stepItems = [\n  { title: 'profile.step.name' },\n  { title: 'profile.step.general' },\n  { title: 'profile.step.inbounds' },\n  { title: 'profile.step.outbounds' },\n  { title: 'profile.step.route' },\n  { title: 'profile.step.dns' },\n  { title: 'profile.step.mixin-script' },\n] as const\n\nconst profile = ref<IProfile>(profilesStore.getProfileTemplate())\n\nconst inboundOptions = computed(() =>\n  profile.value.inbounds.map((v) => ({ label: v.tag, value: v.id })),\n)\n\nconst outboundOptions = computed(() =>\n  profile.value.outbounds.map((v) => ({ label: v.tag, value: v.id })),\n)\n\nconst serverOptions = computed(() =>\n  profile.value.dns.servers.map((v) => ({ label: v.tag, value: v.id })),\n)\n\nconst generalConfig = computed({\n  get() {\n    return { log: profile.value.log, experimental: profile.value.experimental }\n  },\n  set({ log, experimental }) {\n    profile.value.log = log\n    profile.value.experimental = experimental\n  },\n})\n\nconst mixinAndScriptConfig = computed({\n  get() {\n    return { mixin: profile.value.mixin, script: profile.value.script }\n  },\n  set({ mixin, script }) {\n    profile.value.mixin = mixin\n    profile.value.script = script\n  },\n})\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\nconst handlePrevStep = () => currentStep.value--\nconst handleNextStep = () => currentStep.value++\n\nconst handleSave = async () => {\n  loading.value = true\n  try {\n    if (props.id) {\n      await profilesStore.editProfile(props.id, profile.value)\n    } else {\n      await profilesStore.addProfile(profile.value)\n    }\n    await handleSubmit()\n  } catch (error: any) {\n    console.error('handleSave: ', error)\n    message.error(error)\n  }\n  loading.value = false\n}\n\nconst handleAdd = () => {\n  const map: Record<number, Ref> = {\n    [Step.Inbounds]: inboundsRef,\n    [Step.Outbounds]: outboundsRef,\n    [Step.Route]: routeRef,\n    [Step.Dns]: dnsRef,\n  }\n  map[currentStep.value]!.value.handleAdd()\n}\n\nconst handlePreview = async () => {\n  try {\n    const config = await generateConfig(profile.value)\n    alert(profile.value.name, JSON.stringify(config, null, 2))\n  } catch (error: any) {\n    message.error(error.message || error)\n  }\n}\n\nif (props.id) {\n  const p = profilesStore.getProfileById(props.id)\n  if (p) {\n    profile.value = deepClone(p)\n  }\n}\n\nconst modalSlots = {\n  title: () =>\n    h(\n      Dropdown,\n      {},\n      {\n        default: () =>\n          h(\n            'div',\n            {\n              class: 'font-bold',\n            },\n            `${t(stepItems[currentStep.value]!.title)} （${currentStep.value + 1} / ${stepItems.length}）`,\n          ),\n        overlay: () =>\n          h(\n            'div',\n            {\n              class: 'p-4 flex flex-col',\n            },\n            stepItems.map((step, index) =>\n              h(\n                Button,\n                {\n                  type: currentStep.value === index ? 'link' : 'text',\n                  disabled: !profile.value.name && currentStep.value !== index,\n                  onClick: () => (currentStep.value = index),\n                },\n                () => t(step.title),\n              ),\n            ),\n          ),\n      },\n    ),\n\n  toolbar: () => [\n    h(Button, {\n      type: 'text',\n      icon: 'file',\n      onClick: handlePreview,\n    }),\n    h(Button, {\n      type: 'text',\n      icon: 'add',\n      style: {\n        display: [Step.Inbounds, Step.Outbounds, Step.Route, Step.Dns].includes(currentStep.value)\n          ? ''\n          : 'none',\n      },\n      onClick: handleAdd,\n    }),\n  ],\n  action: () => [\n    h(\n      Button,\n      {\n        disabled: currentStep.value === Step.Name,\n        onClick: handlePrevStep,\n      },\n      () => t('common.prevStep'),\n    ),\n    h(\n      Button,\n      {\n        class: 'mr-auto',\n        disabled: !profile.value.name || currentStep.value === stepItems.length - 1,\n        onClick: handleNextStep,\n      },\n      () => t('common.nextStep'),\n    ),\n  ],\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        disabled: !profile.value.name,\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div>\n    <div v-if=\"currentStep === Step.Name\">\n      <Input\n        v-model=\"profile.name\"\n        autofocus\n        :border=\"false\"\n        :placeholder=\"t('profile.name')\"\n        class=\"w-full\"\n      />\n    </div>\n    <div v-if=\"currentStep === Step.General\">\n      <GeneralConfig v-model=\"generalConfig\" :outbound-options=\"outboundOptions\" />\n    </div>\n    <div v-if=\"currentStep === Step.Inbounds\">\n      <InboundsConfig ref=\"inboundsRef\" v-model=\"profile.inbounds\" />\n    </div>\n    <div v-if=\"currentStep === Step.Outbounds\">\n      <OutboundsConfig ref=\"outboundsRef\" v-model=\"profile.outbounds\" />\n    </div>\n    <div v-if=\"currentStep === Step.Route\">\n      <RouteConfig\n        ref=\"routeRef\"\n        v-model=\"profile.route\"\n        :inbound-options=\"inboundOptions\"\n        :outbound-options=\"outboundOptions\"\n        :server-options=\"serverOptions\"\n      />\n    </div>\n    <div v-if=\"currentStep === Step.Dns\">\n      <DnsConfig\n        ref=\"dnsRef\"\n        v-model=\"profile.dns\"\n        :inbound-options=\"inboundOptions\"\n        :outbound-options=\"outboundOptions\"\n        :rule-set=\"profile.route.rule_set\"\n      />\n    </div>\n    <div v-if=\"currentStep === Step.MixinScript\">\n      <MixinAndScript v-model=\"mixinAndScriptConfig\" />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/RouteConfig.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, useTemplateRef } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport RouteRulesConfig from './RouteRulesConfig.vue'\nimport RouteRulesetConfig from './RouteRulesetConfig.vue'\n\ninterface Props {\n  inboundOptions: { label: string; value: string }[]\n  outboundOptions: { label: string; value: string }[]\n  serverOptions: { label: string; value: string }[]\n}\n\ndefineProps<Props>()\n\nconst model = defineModel<IProfile['route']>({ required: true })\n\nconst activeKey = ref('common')\nconst rulesConfigRef = useTemplateRef('rulesConfigRef')\nconst rulesetConfigRef = useTemplateRef('rulesetConfigRef')\nconst tabs = [\n  { key: 'common', tab: 'kernel.route.tab.common' },\n  { key: 'rule_set', tab: 'kernel.route.tab.rule_set' },\n  { key: 'rules', tab: 'kernel.route.tab.rules' },\n]\n\nconst { t } = useI18n()\n\nconst handleAdd = () => {\n  const handlerMap: Record<string, (() => void) | undefined> = {\n    common: () => {},\n    rules: rulesConfigRef.value?.handleAdd,\n    rule_set: rulesetConfigRef.value?.handleAdd,\n  }\n  handlerMap[activeKey.value]?.()\n}\n\ndefineExpose({ handleAdd })\n</script>\n\n<template>\n  <Tabs v-model:active-key=\"activeKey\" :items=\"tabs\" tab-position=\"top\">\n    <template #common>\n      <div class=\"form-item\">\n        {{ t('kernel.route.find_process') }}\n        <Switch v-model=\"model.find_process\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.route.auto_detect_interface') }}\n        <Switch v-model=\"model.auto_detect_interface\" />\n      </div>\n      <div v-if=\"!model.auto_detect_interface\" class=\"form-item\">\n        {{ t('kernel.route.default_interface') }}\n        <InterfaceSelect v-model=\"model.default_interface\" clearable />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.route.default_domain_resolver.server') }}\n        <Select v-model=\"model.default_domain_resolver.server\" :options=\"serverOptions\" clearable />\n      </div>\n      <!-- <div class=\"form-item\">\n        {{ t('kernel.route.default_domain_resolver.client_subnet') }}\n        <Input v-model=\"model.default_domain_resolver.client_subnet\" editable />\n      </div> -->\n      <div class=\"form-item\">\n        {{ t('kernel.route.final') }}\n        <Select v-model=\"model.final\" :options=\"outboundOptions\" clearable />\n      </div>\n    </template>\n    <template #rule_set>\n      <RouteRulesetConfig\n        ref=\"rulesetConfigRef\"\n        v-model=\"model.rule_set\"\n        :outbound-options=\"outboundOptions\"\n      />\n    </template>\n    <template #rules>\n      <RouteRulesConfig\n        ref=\"rulesConfigRef\"\n        v-model=\"model.rules\"\n        :inbound-options=\"inboundOptions\"\n        :outbound-options=\"outboundOptions\"\n        :server-options=\"serverOptions\"\n        :rule-set=\"model.rule_set\"\n      />\n    </template>\n  </Tabs>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/RouteRulesConfig.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { DraggableOptions } from '@/constant/app'\nimport { RuleActionRejectOptions } from '@/constant/kernel'\nimport {\n  DomainStrategyOptions,\n  RuleActionOptions,\n  RuleSnifferOptions,\n  RulesTypeOptions,\n} from '@/constant/kernel'\nimport { DefaultRouteRule } from '@/constant/profile'\nimport {\n  RuleAction,\n  RulesetFormat,\n  RulesetType,\n  RuleType,\n  ClashMode,\n  Strategy,\n} from '@/enums/kernel'\nimport { useBool } from '@/hooks'\nimport { deepClone, message } from '@/utils'\n\ninterface Props {\n  inboundOptions: { label: string; value: string }[]\n  outboundOptions: { label: string; value: string }[]\n  serverOptions: { label: string; value: string }[]\n  ruleSet: IRuleSet[]\n}\n\nconst props = defineProps<Props>()\n\nconst model = defineModel<IRule[]>({ required: true })\n\nlet ruleId = 0\nconst fields = ref<IRule>(DefaultRouteRule())\n\nconst { t } = useI18n()\nconst [showEditModal] = useBool(false)\n\nconst handleAdd = () => {\n  ruleId = -1\n  fields.value = DefaultRouteRule()\n  showEditModal.value = true\n}\n\ndefineExpose({ handleAdd })\n\nconst handleAddInsertionPoint = () => {\n  model.value.unshift({\n    id: RuleType.InsertionPoint,\n    type: RuleType.InsertionPoint,\n    enable: true,\n    payload: '',\n    invert: false,\n    action: RuleAction.Sniff,\n    outbound: '',\n    sniffer: [],\n    strategy: Strategy.Default,\n    server: '',\n  })\n}\n\nconst handleAddEnd = () => {\n  if (ruleId !== -1) {\n    model.value[ruleId] = fields.value\n  } else {\n    const index = model.value.findIndex((v) => v.type === RuleType.InsertionPoint)\n    if (index !== -1) {\n      model.value.splice(index + 1, 0, fields.value)\n    } else {\n      model.value.unshift(fields.value)\n    }\n  }\n}\n\nconst handleEdit = (index: number) => {\n  ruleId = index\n  fields.value = deepClone(model.value[index]!)\n  showEditModal.value = true\n}\n\nconst handleUse = (ruleset: any) => {\n  const ids = fields.value.payload.split(',').filter((v) => v)\n  const idx = ids.findIndex((v) => v === ruleset.id)\n  if (idx === -1) {\n    ids.push(ruleset.id)\n  } else {\n    ids.splice(idx, 1)\n  }\n  fields.value.payload = ids.join(',')\n}\n\nconst handleClearRuleset = (ruleset: any) => {\n  const ids = fields.value.payload.split(',').filter((id) => props.ruleSet.find((v) => v.id === id))\n  ruleset.payload = ids.join(',')\n}\n\nconst handleDelete = (index: number) => {\n  model.value.splice(index, 1)\n}\n\nconst showLost = () => message.warn('kernel.route.rules.invalid')\n\nconst isSupportPayload = computed(() => {\n  return ![RuleType.RuleSet].includes(fields.value.type as any)\n})\n\nconst isInsertionPointMissing = computed(\n  () => model.value.findIndex((rule) => rule.type === RuleType.InsertionPoint) === -1,\n)\n\nconst hasLost = (rule: IRule) => {\n  const rulesValidationFlags: boolean[] = []\n  const hasMissingInbound = !props.inboundOptions.find((v) => v.value === rule.payload)\n  const hasMissingOutbound = !props.outboundOptions.find((v) => v.value === rule.outbound)\n  const hasMissingRuleset = rule.payload\n    .split(',')\n    .some((id) => !props.ruleSet.find((v) => v.id === id))\n  if (rule.action === RuleAction.Route) {\n    rulesValidationFlags.push(hasMissingOutbound)\n  } else if (rule.action === RuleAction.RouteOptions) {\n    let isValid = true\n    try {\n      JSON.parse(rule.outbound)\n    } catch {\n      isValid = false\n    }\n    rulesValidationFlags.push(!isValid)\n  }\n  if (rule.type === RuleType.Inbound) {\n    rulesValidationFlags.push(hasMissingInbound)\n  } else if (rule.type === RuleType.IpIsPrivate) {\n    rulesValidationFlags.push(!['true', 'false'].includes(rule.payload))\n  } else if (rule.type === RuleType.RuleSet) {\n    rulesValidationFlags.push(hasMissingRuleset)\n  }\n  return rulesValidationFlags.some((v) => v) || !rule.payload\n}\n\nconst renderRule = (rule: IRule) => {\n  const { type, payload, outbound, action, invert } = rule\n  const children: string[] = [type]\n  let _payload = payload\n  if (type === RuleType.RuleSet) {\n    _payload = rule.payload\n      .split(',')\n      .map((id) => props.ruleSet.find((v) => v.id === id)?.tag || id)\n      .join(',')\n  } else if (type === RuleType.Inbound) {\n    _payload = props.inboundOptions.find((v) => v.value === rule.payload)?.label || rule.payload\n  }\n  if (invert) {\n    _payload += ` (invert) `\n  }\n  children.push(_payload, action)\n  if (outbound) {\n    const proxy = props.outboundOptions.find((v) => v.value === outbound)?.label || outbound\n    children.push(proxy)\n  }\n  return children.join(',')\n}\n</script>\n\n<template>\n  <Empty v-if=\"model.length === 0 || (model.length === 1 && !isInsertionPointMissing)\">\n    <template #description>\n      <Button icon=\"add\" type=\"primary\" size=\"small\" @click=\"handleAdd\">\n        {{ t('common.add') }}\n      </Button>\n    </template>\n  </Empty>\n\n  <Divider v-if=\"isInsertionPointMissing\">\n    <Button type=\"text\" size=\"small\" @click=\"handleAddInsertionPoint\">\n      {{ t('kernel.addInsertionPoint') }}\n    </Button>\n  </Divider>\n\n  <div v-draggable=\"[model, DraggableOptions]\">\n    <Card v-for=\"(rule, index) in model\" :key=\"rule.id\" class=\"mb-2\">\n      <div v-if=\"rule.type === RuleType.InsertionPoint\" class=\"text-center font-bold\">\n        <Divider class=\"cursor-move\">\n          <Button icon=\"add\" type=\"text\" size=\"small\" @click=\"handleAdd\">\n            {{ t('kernel.insertionPoint') }}\n          </Button>\n        </Divider>\n      </div>\n      <div v-else class=\"flex items-center py-2 gap-8\">\n        <Switch v-model=\"rule.enable\" border=\"square\" size=\"small\" />\n        <div class=\"font-bold\">\n          <span\n            v-if=\"hasLost(rule)\"\n            class=\"cursor-pointer\"\n            :style=\"{ color: 'rgb(200, 193, 11)' }\"\n            @click=\"showLost\"\n          >\n            [ ! ]\n          </span>\n          {{ renderRule(rule) }}\n        </div>\n        <div class=\"ml-auto\">\n          <Button\n            v-if=\"rule.type === RuleType.RuleSet && rule.payload && hasLost(rule)\"\n            type=\"text\"\n            @click=\"handleClearRuleset(rule)\"\n          >\n            {{ t('common.clear') }}\n          </Button>\n          <Button icon=\"edit\" type=\"text\" size=\"small\" @click=\"handleEdit(index)\" />\n          <Button icon=\"delete\" type=\"text\" size=\"small\" @click=\"handleDelete(index)\" />\n        </div>\n      </div>\n    </Card>\n  </div>\n\n  <Modal\n    v-model:open=\"showEditModal\"\n    :on-ok=\"handleAddEnd\"\n    title=\"kernel.route.tab.rules\"\n    max-width=\"80\"\n    max-height=\"80\"\n  >\n    <div class=\"form-item\">\n      {{ t('kernel.route.rules.type') }}\n      <Select v-model=\"fields.type\" :options=\"RulesTypeOptions\" />\n    </div>\n    <div class=\"form-item\">\n      {{ t('kernel.route.rules.action.name') }}\n      <Radio v-model=\"fields.action\" :options=\"RuleActionOptions\" class=\"ml-8\" />\n    </div>\n    <div v-if=\"isSupportPayload\" class=\"form-item\">\n      {{ t('kernel.route.rules.payload') }}\n      <Radio\n        v-if=\"fields.type === RuleType.ClashMode\"\n        v-model=\"fields.payload\"\n        :options=\"[\n          {\n            label: 'kernel.global',\n            value: ClashMode.Global,\n          },\n          {\n            label: 'kernel.direct',\n            value: ClashMode.Direct,\n          },\n        ]\"\n      />\n      <Select\n        v-else-if=\"fields.type === RuleType.Inbound\"\n        v-model=\"fields.payload\"\n        :options=\"inboundOptions\"\n      />\n      <CodeViewer\n        v-else-if=\"fields.type === RuleType.Inline\"\n        v-model=\"fields.payload\"\n        editable\n        lang=\"json\"\n        style=\"min-width: 320px\"\n      />\n      <Switch\n        v-else-if=\"fields.type === RuleType.IpIsPrivate\"\n        :model-value=\"fields.payload === 'true'\"\n        @change=\"(val) => (fields.payload = val ? 'true' : 'false')\"\n      />\n      <Input v-else v-model=\"fields.payload\" autofocus />\n    </div>\n    <div class=\"form-item\">\n      {{ t('kernel.route.rules.invert') }}\n      <Switch v-model=\"fields.invert\" />\n    </div>\n    <Card class=\"mt-4 mb-16\">\n      <template v-if=\"fields.action === RuleAction.Route\">\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.outbound') }}\n          <Select v-model=\"fields.outbound\" :options=\"outboundOptions\" clearable />\n        </div>\n      </template>\n      <template v-else-if=\"fields.action === RuleAction.RouteOptions\">\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.routeOptions') }}\n          <CodeViewer v-model=\"fields.outbound\" editable lang=\"json\" style=\"min-width: 320px\" />\n        </div>\n      </template>\n      <template v-else-if=\"fields.action === RuleAction.Reject\">\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.action.rejectMethod') }}\n          <Radio v-model=\"fields.outbound\" :options=\"RuleActionRejectOptions\" />\n        </div>\n      </template>\n      <template v-else-if=\"fields.action === RuleAction.HijackDNS\">\n        <Empty description=\"common.none\" />\n      </template>\n      <template v-else-if=\"fields.action === RuleAction.Sniff\">\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.sniffer.name') }}\n          <Select\n            v-model=\"fields.sniffer\"\n            multiple\n            clearable\n            :options=\"RuleSnifferOptions\"\n            placeholder=\"All\"\n          />\n        </div>\n      </template>\n      <template v-else-if=\"fields.action === RuleAction.Resolve\">\n        <div class=\"form-item\">\n          {{ t('kernel.strategy.name') }}\n          <Select v-model=\"fields.strategy\" :options=\"DomainStrategyOptions\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('kernel.route.rules.server') }}\n          <Select\n            v-model=\"fields.server\"\n            :options=\"[{ label: 'kernel.strategy.byDnsRules', value: '' }, ...serverOptions]\"\n          />\n        </div>\n      </template>\n    </Card>\n    <template v-if=\"fields.type === RuleType.RuleSet\">\n      <Divider>{{ t('kernel.route.tab.rule_set') }}</Divider>\n      <div class=\"grid grid-cols-3 gap-8\">\n        <Empty v-if=\"ruleSet.length === 0\" :description=\"t('kernel.route.rule_set.empty')\" />\n        <template v-else>\n          <Card\n            v-for=\"ruleset in ruleSet\"\n            :key=\"ruleset.tag\"\n            v-tips=\"ruleset.type\"\n            :title=\"ruleset.tag\"\n            :selected=\"fields.payload.includes(ruleset.id)\"\n            class=\"ruleset\"\n            @click=\"handleUse(ruleset)\"\n          >\n            <div class=\"text-12\">\n              {{ ruleset.type }}\n              {{ ruleset.type === RulesetType.Inline ? RulesetFormat.Source : ruleset.format }}\n            </div>\n          </Card>\n        </template>\n      </div>\n    </template>\n  </Modal>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/components/RouteRulesetConfig.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { DraggableOptions } from '@/constant/app'\nimport { RulesetFormatOptions, RulesetTypeOptions } from '@/constant/kernel'\nimport { DefaultRouteRuleset } from '@/constant/profile'\nimport { RulesetFormat, RulesetType } from '@/enums/kernel'\nimport { useBool } from '@/hooks'\nimport { useRulesetsStore } from '@/stores'\nimport { deepClone, message } from '@/utils'\n\ninterface Props {\n  outboundOptions: { label: string; value: string }[]\n}\n\ndefineProps<Props>()\n\nconst model = defineModel<IRuleSet[]>({ required: true })\n\nlet rulesetId = 0\nconst fields = ref<IRuleSet>(DefaultRouteRuleset())\n\nconst { t } = useI18n()\nconst [showEditModal] = useBool(false)\nconst rulesetsStore = useRulesetsStore()\n\nconst handleAdd = () => {\n  rulesetId = -1\n  fields.value = DefaultRouteRuleset()\n  showEditModal.value = true\n}\n\ndefineExpose({ handleAdd })\n\nconst handleAddEnd = () => {\n  if (rulesetId !== -1) {\n    model.value[rulesetId] = fields.value\n  } else {\n    model.value.unshift(fields.value)\n  }\n}\n\nconst handleEdit = (index: number) => {\n  rulesetId = index\n  fields.value = deepClone(model.value[index]!)\n  showEditModal.value = true\n}\n\nconst handleDelete = (index: number) => {\n  model.value.splice(index, 1)\n}\n\nconst showLost = () => message.warn('kernel.route.rule_set.notFound')\n\nconst hasLost = (ruleset: IRuleSet) => {\n  if (ruleset.type !== RulesetType.Local) return false\n  return !rulesetsStore.getRulesetById(ruleset.path)\n}\n\nconst handleUse = (ruleset: any) => {\n  fields.value.path = ruleset.id\n  fields.value.tag = ruleset.tag\n  fields.value.format = ruleset.format\n}\n</script>\n\n<template>\n  <Empty v-if=\"model.length === 0\">\n    <template #description>\n      <Button icon=\"add\" type=\"primary\" size=\"small\" @click=\"handleAdd\">\n        {{ t('common.add') }}\n      </Button>\n    </template>\n  </Empty>\n\n  <div v-draggable=\"[model, DraggableOptions]\">\n    <Card v-for=\"(ruleset, index) in model\" :key=\"ruleset.id\" class=\"mb-2\">\n      <div class=\"flex items-center py-2\">\n        <div class=\"font-bold\">\n          <span\n            v-if=\"hasLost(ruleset)\"\n            class=\"cursor-pointer\"\n            :style=\"{ color: 'rgb(200, 193, 11)' }\"\n            @click=\"showLost\"\n          >\n            [ ! ]\n          </span>\n          <Tag color=\"cyan\">{{ ruleset.tag }}</Tag>\n          <Tag>\n            {{ t('kernel.route.rule_set.type.' + ruleset.type) }}\n            {{\n              t(\n                'kernel.route.rule_set.format.' +\n                  (ruleset.type === RulesetType.Inline ? RulesetFormat.Source : ruleset.format),\n              )\n            }}\n          </Tag>\n          <template v-if=\"ruleset.type === RulesetType.Inline\">\n            {{ ruleset.rules }}\n          </template>\n        </div>\n        <div class=\"ml-auto\">\n          <Button icon=\"edit\" type=\"text\" size=\"small\" @click=\"handleEdit(index)\" />\n          <Button icon=\"delete\" type=\"text\" size=\"small\" @click=\"handleDelete(index)\" />\n        </div>\n      </div>\n    </Card>\n  </div>\n\n  <Modal\n    v-model:open=\"showEditModal\"\n    :on-ok=\"handleAddEnd\"\n    title=\"kernel.route.tab.rule_set\"\n    max-width=\"80\"\n    max-height=\"80\"\n  >\n    <div class=\"form-item\">\n      {{ t('kernel.route.rule_set.tag') }}\n      <Input v-model=\"fields.tag\" autofocus />\n    </div>\n    <div class=\"form-item\">\n      {{ t('kernel.route.rule_set.type.name') }}\n      <Radio v-model=\"fields.type\" :options=\"RulesetTypeOptions\" />\n    </div>\n    <template v-if=\"fields.type === RulesetType.Local\">\n      <Divider>{{ t('kernel.route.tab.rule_set') }}</Divider>\n      <div class=\"grid grid-cols-3 gap-8\">\n        <Empty\n          v-if=\"rulesetsStore.rulesets.length === 0\"\n          :description=\"t('kernel.route.rule_set.empty')\"\n        />\n        <template v-else>\n          <Card\n            v-for=\"ruleset in rulesetsStore.rulesets\"\n            :key=\"ruleset.tag\"\n            v-tips=\"ruleset.path\"\n            :title=\"ruleset.tag\"\n            :selected=\"fields.path === ruleset.id\"\n            @click=\"handleUse(ruleset)\"\n          >\n            <div class=\"text-12\">\n              {{ ruleset.path }}\n            </div>\n          </Card>\n        </template>\n      </div>\n    </template>\n    <template v-else-if=\"fields.type === RulesetType.Remote\">\n      <div class=\"form-item\">\n        {{ t('kernel.route.rule_set.format.name') }}\n        <Radio v-model=\"fields.format\" :options=\"RulesetFormatOptions\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.route.rule_set.url') }}\n        <Input v-model=\"fields.url\" />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.route.rule_set.download_detour') }}\n        <Select v-model=\"fields.download_detour\" :options=\"outboundOptions\" clearable />\n      </div>\n      <div class=\"form-item\">\n        {{ t('kernel.route.rule_set.update_interval') }}\n        <Input v-model=\"fields.update_interval\" editable />\n      </div>\n    </template>\n    <template v-else-if=\"fields.type === RulesetType.Inline\">\n      <CodeViewer v-model=\"fields.rules\" lang=\"json\" editable />\n    </template>\n  </Modal>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ProfilesView/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { defineAsyncComponent } from 'vue'\nimport { useI18n, I18nT } from 'vue-i18n'\n\nimport { ClipboardSetText } from '@/bridge'\nimport { DraggableOptions, ViewOptions } from '@/constant/app'\nimport { View } from '@/enums/app'\nimport {\n  useProfilesStore,\n  useAppSettingsStore,\n  useKernelApiStore,\n  useSubscribesStore,\n  usePluginsStore,\n  useAppStore,\n} from '@/stores'\nimport { debounce, deepClone, generateConfig, message, sampleID, alert } from '@/utils'\n\nimport { useModal } from '@/components/Modal'\n\nimport type { Menu } from '@/types/app'\n\nconst ProfileForm = defineAsyncComponent(() => import('./components/ProfileForm.vue'))\nconst ProfileEditor = defineAsyncComponent(() => import('./components/ProfileEditor.vue'))\n\nconst { t } = useI18n()\nconst [Modal, modalApi] = useModal({})\nconst appStore = useAppStore()\nconst profilesStore = useProfilesStore()\nconst subscribesStore = useSubscribesStore()\nconst appSettingsStore = useAppSettingsStore()\nconst kernelApiStore = useKernelApiStore()\nconst pluginsStore = usePluginsStore()\n\nconst menuList: Menu[] = [\n  'profile.step.name',\n  'profile.step.general',\n  'profile.step.inbounds',\n  'profile.step.outbounds',\n  'profile.step.route',\n  'profile.step.dns',\n  'profile.step.mixin-script',\n].map((v, i) => {\n  return {\n    label: v,\n    handler: (id: string) => {\n      const p = profilesStore.getProfileById(id)\n      p && handleShowProfileForm(p.id, i)\n    },\n  }\n})\n\nconst secondaryMenusList: Menu[] = [\n  {\n    label: 'profiles.start',\n    handler: async (id: string) => {\n      appSettingsStore.app.kernel.profile = id\n      try {\n        const e = await kernelApiStore.stopCore().catch((e) => e)\n        if (e && e !== 'The core is not running') {\n          throw e\n        }\n        await kernelApiStore.startCore()\n      } catch (error: any) {\n        message.error(error)\n        console.error(error)\n      }\n    },\n  },\n  {\n    label: 'profiles.copy',\n    handler: async (id: string) => {\n      const p = deepClone(profilesStore.getProfileById(id)!)\n      p.id = sampleID()\n      p.name = p.name + '(Copy)'\n      profilesStore.addProfile(p)\n      message.success('common.success')\n    },\n  },\n  {\n    label: 'profiles.copytoClipboard',\n    handler: async (id: string) => {\n      const p = profilesStore.getProfileById(id)!\n      try {\n        const config = await generateConfig(p)\n        const str = JSON.stringify(config, null, 2)\n        const ok = await ClipboardSetText(str)\n        if (!ok) throw 'ClipboardSetText Error'\n        message.success('common.success')\n      } catch (error: any) {\n        message.error(error.message || error)\n      }\n    },\n  },\n  {\n    label: 'profiles.generateAndView',\n    handler: async (id: string) => {\n      const p = profilesStore.getProfileById(id)!\n      try {\n        const config = await generateConfig(p)\n        alert(p.name, JSON.stringify(config, null, 2))\n      } catch (error: any) {\n        message.error(error.message || error)\n      }\n    },\n  },\n  {\n    label: 'Manual Edit (Beta)',\n    handler: async (id: string) => {\n      const profile = profilesStore.getProfileById(id)!\n      modalApi.setProps({ title: profile.name, width: '90', height: '90' })\n      modalApi.setContent(ProfileEditor, { profile }).open()\n    },\n  },\n]\n\nconst generateMenus = (profile: IProfile) => {\n  const moreMenus: Menu[] = secondaryMenusList.map((v) => ({\n    ...v,\n    handler: () => v.handler?.(profile.id),\n  }))\n  const builtInMenus: Menu[] = [\n    ...menuList.map((v) => ({ ...v, handler: () => v.handler?.(profile.id) })),\n    {\n      label: '',\n      separator: true,\n    },\n    {\n      label: 'common.more',\n      children: moreMenus,\n    },\n  ]\n\n  const contextMenus = pluginsStore.plugins.filter(\n    (plugin) => Object.keys(plugin.context.profiles).length !== 0,\n  )\n\n  if (contextMenus.length !== 0) {\n    moreMenus.push(\n      {\n        label: '',\n        separator: true,\n      },\n      ...contextMenus.reduce((prev, plugin) => {\n        const menus = Object.entries(plugin.context.profiles)\n        return prev.concat(\n          menus.map(([title, fn]) => {\n            return {\n              label: title,\n              handler: async () => {\n                try {\n                  plugin.running = true\n                  await pluginsStore.manualTrigger(plugin.id, fn as any, profile)\n                } catch (error: any) {\n                  message.error(error)\n                } finally {\n                  plugin.running = false\n                }\n              },\n            }\n          }),\n        )\n      }, [] as Menu[]),\n    )\n  }\n\n  return builtInMenus\n}\n\nconst handleShowProfileForm = (id?: string, step = 0) => {\n  modalApi.setProps({ minWidth: '70' })\n  modalApi.setContent(ProfileForm, { id, step }).open()\n}\n\nconst handleDeleteProfile = async (p: IProfile) => {\n  const { profile } = appSettingsStore.app.kernel\n  if (profile === p.id && kernelApiStore.running) {\n    message.warn('profiles.shouldStop')\n    return\n  }\n\n  try {\n    await profilesStore.deleteProfile(p.id)\n  } catch (error: any) {\n    console.error('deleteProfile: ', error)\n    message.error(error)\n  }\n}\n\nconst handleUseProfile = async (p: IProfile) => {\n  if (appSettingsStore.app.kernel.profile === p.id) return\n\n  appSettingsStore.app.kernel.profile = p.id\n\n  if (kernelApiStore.running) {\n    await kernelApiStore.restartCore()\n  }\n}\n\nconst isCreatedBySubscription = (id: string) => {\n  return !!subscribesStore.getSubscribeById(id)\n}\n\nconst showAuto = () => alert('Tips', 'profile.auto')\n\nconst onSortUpdate = debounce(profilesStore.saveProfiles, 1000)\n</script>\n\n<template>\n  <div v-if=\"profilesStore.profiles.length === 0\" class=\"grid-list-empty\">\n    <Empty>\n      <template #description>\n        <I18nT keypath=\"profiles.empty\" tag=\"div\" scope=\"global\" class=\"flex items-center mt-12\">\n          <template #action>\n            <Button type=\"link\" @click=\"handleShowProfileForm()\">{{ t('common.add') }}</Button>\n          </template>\n        </I18nT>\n        <div class=\"flex items-center\">\n          <CustomAction :actions=\"appStore.customActions.profiles_header\" />\n        </div>\n      </template>\n    </Empty>\n  </div>\n\n  <div v-else class=\"grid-list-header\">\n    <Radio v-model=\"appSettingsStore.app.profilesView\" :options=\"ViewOptions\" class=\"mr-auto\" />\n    <CustomAction :actions=\"appStore.customActions.profiles_header\" />\n    <Button type=\"primary\" icon=\"add\" @click=\"handleShowProfileForm()\">\n      {{ t('common.add') }}\n    </Button>\n  </div>\n\n  <div\n    v-draggable=\"[profilesStore.profiles, { ...DraggableOptions, onUpdate: onSortUpdate }]\"\n    :class=\"'grid-list-' + appSettingsStore.app.profilesView\"\n  >\n    <Card\n      v-for=\"p in profilesStore.profiles\"\n      :key=\"p.id\"\n      v-menu=\"generateMenus(p)\"\n      :title=\"p.name\"\n      :selected=\"appSettingsStore.app.kernel.profile === p.id\"\n      class=\"grid-list-item\"\n      @dblclick=\"handleUseProfile(p)\"\n    >\n      <template #title-prefix>\n        <Tag\n          v-if=\"isCreatedBySubscription(p.id)\"\n          color=\"primary\"\n          size=\"small\"\n          style=\"margin-left: 0\"\n          @click=\"showAuto\"\n        >\n          {{ t('common.auto') }}\n        </Tag>\n      </template>\n\n      <template v-if=\"appSettingsStore.app.profilesView === View.Grid\" #extra>\n        <Dropdown>\n          <Button type=\"link\" size=\"small\" icon=\"more\" />\n          <template #overlay>\n            <div class=\"flex flex-col gap-4 min-w-64 p-4\">\n              <Button type=\"text\" @click=\"handleUseProfile(p)\">\n                {{ t('common.use') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleShowProfileForm(p.id)\">\n                {{ t('common.edit') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleDeleteProfile(p)\">\n                {{ t('common.delete') }}\n              </Button>\n            </div>\n          </template>\n        </Dropdown>\n      </template>\n\n      <template v-else #extra>\n        <Button type=\"text\" size=\"small\" @click=\"handleUseProfile(p)\">\n          {{ t('common.use') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" @click=\"handleShowProfileForm(p.id)\">\n          {{ t('common.edit') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" @click=\"handleDeleteProfile(p)\">\n          {{ t('common.delete') }}\n        </Button>\n      </template>\n      <div>\n        {{ t('profiles.inbounds') }}\n        :\n        {{ p.inbounds.length }}\n        /\n        {{ t('profiles.outbounds') }}\n        :\n        {{ p.outbounds.length }}\n      </div>\n      <div>\n        {{ t('kernel.route.tab.rule_set') }}\n        :\n        {{ p.route.rule_set.length }}\n        /\n        {{ t('kernel.route.tab.rules') }}\n        :\n        {{ p.route.rules.length }}\n      </div>\n      <div>\n        {{ t('profiles.dnsServers') }}\n        :\n        {{ p.dns.servers.length }}\n        /\n        {{ t('profiles.dnsRules') }}\n        :\n        {{ p.dns.rules.length }}\n      </div>\n    </Card>\n  </div>\n\n  <Modal />\n</template>\n"
  },
  {
    "path": "frontend/src/views/RulesetsView/components/RulesetForm.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, watch, computed, h } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { RulesetFormatOptions } from '@/constant/kernel'\nimport { RulesetFormat } from '@/enums/kernel'\nimport { type RuleSet, useRulesetsStore } from '@/stores'\nimport { deepClone, message, sampleID } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\ninterface Props {\n  id?: string\n  isUpdate?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  id: '',\n  isUpdate: false,\n})\n\nconst loading = ref(false)\n\nconst ruleset = ref<RuleSet>({\n  id: sampleID(),\n  tag: '',\n  updateTime: 0,\n  format: RulesetFormat.Binary,\n  type: 'Http',\n  url: '',\n  count: 0,\n  path: `data/rulesets/${sampleID()}.srs`,\n  disabled: false,\n})\n\nconst { t } = useI18n()\nconst rulesetsStore = useRulesetsStore()\n\nconst handleCancel = inject('cancel') as any\n\nconst handleSubmit = async () => {\n  loading.value = true\n\n  if (props.isUpdate) {\n    try {\n      await rulesetsStore.editRuleset(props.id, ruleset.value)\n      handleCancel()\n    } catch (error: any) {\n      console.error('editRuleset: ', error)\n      message.error(error)\n    }\n\n    loading.value = true\n\n    return\n  }\n\n  try {\n    await rulesetsStore.addRuleset(ruleset.value)\n    handleCancel()\n  } catch (error: any) {\n    console.error('addRuleset: ', error)\n    message.error(error)\n  }\n\n  loading.value = true\n}\n\nconst disabled = computed(\n  () =>\n    !ruleset.value.tag ||\n    (ruleset.value.type === 'Manual' && !ruleset.value.path) ||\n    (['Http', 'File'].includes(ruleset.value.type) && (!ruleset.value.url || !ruleset.value.path)),\n)\n\nwatch(\n  () => ruleset.value.type,\n  (v) => {\n    if (v === 'Manual') {\n      ruleset.value.format = RulesetFormat.Source\n    }\n  },\n)\n\nwatch(\n  () => ruleset.value.format,\n  (v, old) => {\n    const isJson = v === RulesetFormat.Source\n    if (!isJson && ruleset.value.type === 'Manual') {\n      ruleset.value.format = old\n      message.error('Not support')\n      return\n    }\n    ruleset.value.path = ruleset.value.path.replace(\n      isJson ? '.srs' : '.json',\n      isJson ? '.json' : '.srs',\n    )\n  },\n)\n\nif (props.isUpdate) {\n  const r = rulesetsStore.getRulesetById(props.id)\n  if (r) {\n    ruleset.value = deepClone(r)\n  }\n}\n\nconst modalSlots = {\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        disabled: disabled.value,\n        loading: loading.value,\n        onClick: handleSubmit,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div>\n    <div class=\"form-item\">\n      {{ t('ruleset.rulesetType') }}\n      <Radio\n        v-model=\"ruleset.type\"\n        :options=\"[\n          { label: 'common.http', value: 'Http' },\n          { label: 'common.file', value: 'File' },\n          { label: 'ruleset.manual', value: 'Manual' },\n        ]\"\n      />\n    </div>\n    <div v-show=\"ruleset.type !== 'Manual'\" class=\"form-item\">\n      {{ t('ruleset.format.name') }}\n      <Radio v-model=\"ruleset.format\" :options=\"RulesetFormatOptions\" />\n    </div>\n    <div class=\"form-item\">\n      {{ t('ruleset.name') }} *\n      <div class=\"min-w-[75%]\">\n        <Input v-model=\"ruleset.tag\" autofocus class=\"w-full\" />\n      </div>\n    </div>\n    <div v-show=\"ruleset.type !== 'Manual'\" class=\"form-item\">\n      {{ t('ruleset.url') }} *\n      <div class=\"min-w-[75%]\">\n        <Input\n          v-model=\"ruleset.url\"\n          allow-paste\n          :placeholder=\"\n            ruleset.type === 'Http'\n              ? 'http(s)://'\n              : 'data/local/{filename}.' +\n                (ruleset.format === RulesetFormat.Binary ? 'srs' : 'json')\n          \"\n          class=\"w-full\"\n        />\n      </div>\n    </div>\n    <div class=\"form-item\">\n      {{ t('ruleset.path') }} *\n      <div class=\"min-w-[75%]\">\n        <Input\n          v-model=\"ruleset.path\"\n          :placeholder=\"`data/rulesets/{filename}.${ruleset.format === RulesetFormat.Binary ? 'srs' : 'json'}`\"\n          class=\"w-full\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/RulesetsView/components/RulesetHub.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, h, inject, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { HttpGet } from '@/bridge'\nimport { RulesetFormat } from '@/enums/kernel'\nimport { useRulesetsStore, type RuleSetHub } from '@/stores'\nimport { message, alert } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\nimport Pagination from '@/components/Pagination/index.vue'\n\nconst pageSize = 27\nconst currentPage = ref(1)\n\nconst { t } = useI18n()\nconst rulesetsStore = useRulesetsStore()\n\nconst keywords = ref('')\nconst handleCancel = inject('cancel') as any\n\nwatch(keywords, () => (currentPage.value = 1))\n\nconst filteredList = computed(() => {\n  if (!keywords.value) return rulesetsStore.rulesetHub.list\n  return rulesetsStore.rulesetHub.list.filter((ruleset) => ruleset.name.includes(keywords.value))\n})\n\nconst currentList = computed(() => {\n  return filteredList.value.slice(\n    (currentPage.value - 1) * pageSize,\n    (currentPage.value - 1) * pageSize + pageSize,\n  )\n})\n\nconst getRulesetUrlAndSuffix = (ruleset: RuleSetHub['list'][number], format: RulesetFormat) => {\n  const suffix = { [RulesetFormat.Binary]: '.srs', [RulesetFormat.Source]: '.json' }[format]\n  const basrUrl = {\n    geosite: rulesetsStore.rulesetHub.geosite,\n    geoip: rulesetsStore.rulesetHub.geoip,\n  }[ruleset.type]\n  return [basrUrl + ruleset.name + suffix, suffix] as const\n}\n\nconst handleAddRuleset = async (ruleset: RuleSetHub['list'][number], format: RulesetFormat) => {\n  const [url, suffix] = getRulesetUrlAndSuffix(ruleset, format)\n  const id = ruleset.type + '_' + ruleset.name + '.' + format\n  const file = ruleset.type + '_' + ruleset.name + suffix\n  try {\n    await rulesetsStore.addRuleset({\n      id,\n      tag: `${ruleset.name}-${ruleset.type}${suffix}`,\n      updateTime: 0,\n      disabled: false,\n      type: 'Http',\n      format,\n      path: 'data/rulesets/' + file,\n      url,\n      count: ruleset.count,\n    })\n    const { success } = message.info('rulesets.updating')\n    await rulesetsStore.updateRuleset(id)\n    success('common.success')\n  } catch (error: any) {\n    console.error(error)\n    message.error(error.message || error)\n  }\n}\n\nconst handlePreview = async (ruleset: RuleSetHub['list'][number], format: RulesetFormat) => {\n  const { destroy, error } = message.info('rulesets.fetching', 15_000)\n  try {\n    const { body } = await HttpGet(getRulesetUrlAndSuffix(ruleset, format)[0])\n    destroy()\n    await alert(ruleset.name, JSON.stringify(body, null, 2))\n  } catch (err: any) {\n    error(err.message || err)\n    setTimeout(destroy, 2000)\n  }\n}\n\nconst handleUpdatePluginHub = async () => {\n  try {\n    await rulesetsStore.updateRulesetHub()\n    message.success('rulesets.updateSuccess')\n  } catch (err: any) {\n    message.error(err.message || err)\n  }\n}\n\nconst isAlreadyAdded = (id: string) => rulesetsStore.getRulesetById(id)\n\nif (rulesetsStore.rulesetHub.list.length === 0) {\n  rulesetsStore.updateRulesetHub()\n}\n\nconst modalSlots = {\n  action: () =>\n    !rulesetsStore.rulesetHubLoading\n      ? h(Pagination, {\n          current: currentPage.value,\n          'onUpdate:current': (current: number) => (currentPage.value = current),\n          total: filteredList.value.length,\n          pageSize: pageSize,\n          size: 'small',\n          class: 'mr-auto',\n        })\n      : null,\n  close: () =>\n    h(\n      Button,\n      {\n        type: 'text',\n        onClick: handleCancel,\n      },\n      () => t('common.close'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div class=\"h-full\">\n    <div v-if=\"rulesetsStore.rulesetHubLoading\" class=\"flex items-center justify-center h-full\">\n      <Button type=\"text\" loading />\n    </div>\n    <div v-else class=\"flex flex-col h-full\">\n      <div class=\"flex items-center gap-8\">\n        <Input\n          v-model=\"keywords\"\n          :border=\"false\"\n          :placeholder=\"t('rulesets.total') + ': ' + rulesetsStore.rulesetHub.list.length\"\n          clearable\n          size=\"small\"\n          class=\"flex-1\"\n        />\n        <Button icon=\"refresh\" size=\"small\" @click=\"handleUpdatePluginHub\">\n          {{ t('plugins.update') }}\n        </Button>\n      </div>\n\n      <Empty v-if=\"filteredList.length === 0\" />\n\n      <div class=\"overflow-y-auto grid grid-cols-3 text-12 gap-8 mt-8 pb-16 pr-8\">\n        <Card\n          v-for=\"ruleset in currentList\"\n          :key=\"ruleset.name + ruleset.type\"\n          :title=\"ruleset.name\"\n        >\n          <template #extra>\n            <Tag size=\"small\" color=\"cyan\">{{ ruleset.type }}</Tag>\n          </template>\n          <div class=\"flex flex-col h-full\">\n            <div class=\"flex items-center justify-between\">\n              {{ t('rulesets.rulesetCount') }} : {{ ruleset.count }}\n              <Button\n                icon=\"preview\"\n                size=\"small\"\n                type=\"text\"\n                @click=\"handlePreview(ruleset, RulesetFormat.Source)\"\n              />\n            </div>\n            <!-- <div v-tips=\"ruleset.description\" class=\"flex-1 line-clamp-2\">\n              {{ ruleset.description || t('rulesets.noDesc') }}\n            </div> -->\n            <div class=\"flex items-center justify-end\">\n              <template\n                v-if=\"\n                  isAlreadyAdded(ruleset.type + '_' + ruleset.name + '.' + RulesetFormat.Source)\n                \"\n              >\n                <Button type=\"text\" size=\"small\">\n                  {{ t('ruleset.format.source') }} {{ t('common.added') }}\n                </Button>\n              </template>\n              <template v-else>\n                <Button\n                  type=\"link\"\n                  size=\"small\"\n                  @click=\"handleAddRuleset(ruleset, RulesetFormat.Source)\"\n                >\n                  {{ t('common.add') }} {{ t('ruleset.format.source') }}\n                </Button>\n              </template>\n              <template\n                v-if=\"\n                  isAlreadyAdded(ruleset.type + '_' + ruleset.name + '.' + RulesetFormat.Binary)\n                \"\n              >\n                <Button type=\"text\" size=\"small\">\n                  {{ t('ruleset.format.binary') }} {{ t('common.added') }}\n                </Button>\n              </template>\n              <template v-else>\n                <Button\n                  type=\"link\"\n                  size=\"small\"\n                  @click=\"handleAddRuleset(ruleset, RulesetFormat.Binary)\"\n                >\n                  {{ t('common.add') }} {{ t('ruleset.format.binary') }}\n                </Button>\n              </template>\n            </div>\n          </div>\n        </Card>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/RulesetsView/components/RulesetView.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, h } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { ReadFile, WriteFile } from '@/bridge'\nimport { type RuleSet, useRulesetsStore } from '@/stores'\nimport { deepClone, ignoredError, isValidJson, message } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\ninterface Props {\n  id: string\n}\n\nconst props = defineProps<Props>()\n\nconst loading = ref(false)\nconst ruleset = ref<RuleSet>()\nconst rulesetContent = ref<string>('')\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst { t } = useI18n()\nconst rulesetsStore = useRulesetsStore()\n\nconst handleSave = async () => {\n  if (!ruleset.value) return\n  loading.value = true\n  try {\n    if (!isValidJson(rulesetContent.value)) {\n      throw 'syntax error'\n    }\n    await WriteFile(ruleset.value.path, rulesetContent.value)\n    await rulesetsStore.updateRuleset(ruleset.value.id)\n    await handleSubmit()\n  } catch (error: any) {\n    message.error(error)\n    console.log(error)\n  } finally {\n    loading.value = false\n  }\n}\n\nconst initContent = async () => {\n  const r = rulesetsStore.getRulesetById(props.id)\n  if (r) {\n    ruleset.value = deepClone(r)\n    const content = (await ignoredError(ReadFile, r.path)) || ''\n    rulesetContent.value = content\n  }\n}\n\ninitContent()\n\nconst modalSlots = {\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <CodeViewer v-model=\"rulesetContent\" lang=\"json\" editable class=\"h-full\" />\n</template>\n"
  },
  {
    "path": "frontend/src/views/RulesetsView/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent } from 'vue'\nimport { useI18n, I18nT } from 'vue-i18n'\n\nimport { RemoveFile, WriteFile, OpenURI } from '@/bridge'\nimport { DraggableOptions, ViewOptions } from '@/constant/app'\nimport { EmptyRuleSet } from '@/constant/kernel'\nimport { View } from '@/enums/app'\nimport { RulesetFormat } from '@/enums/kernel'\nimport { type RuleSet, useRulesetsStore, useAppSettingsStore, useEnvStore } from '@/stores'\nimport { debounce, formatRelativeTime, ignoredError, formatDate, message } from '@/utils'\n\nimport { useModal } from '@/components/Modal'\n\nimport type { Menu } from '@/types/app'\n\nconst RulesetForm = defineAsyncComponent(() => import('./components/RulesetForm.vue'))\nconst RulesetHub = defineAsyncComponent(() => import('./components/RulesetHub.vue'))\nconst RulesetView = defineAsyncComponent(() => import('./components/RulesetView.vue'))\n\nconst sourceMenuList: Menu[] = [\n  {\n    label: 'rulesets.editRuleset',\n    handler: (id: string) => handleEditRulesetList(id),\n  },\n  {\n    label: 'common.openFile',\n    handler: async (id: string) => {\n      const ruleset = rulesetsStore.getRulesetById(id)\n      await OpenURI(envStore.env.basePath + '/' + ruleset!.path)\n    },\n  },\n  {\n    label: 'common.clear',\n    handler: (id: string) => handleClearRuleset(id),\n  },\n]\n\nconst binaryMenuList: Menu[] = [\n  {\n    label: 'common.none',\n    handler: (id: string) => {\n      console.log(id)\n      message.info('common.none')\n    },\n  },\n]\n\nconst { t } = useI18n()\nconst [Modal, modalApi] = useModal({})\nconst envStore = useEnvStore()\nconst rulesetsStore = useRulesetsStore()\nconst appSettingsStore = useAppSettingsStore()\n\nconst handleImportRuleset = async () => {\n  modalApi.setProps({\n    title: 'rulesets.hub',\n    cancelText: 'common.close',\n    height: '90',\n    width: '90',\n    submit: false,\n    maskClosable: true,\n  })\n  modalApi.setContent(RulesetHub)\n  modalApi.open()\n}\n\nconst handleShowRulesetForm = async (id?: string, isUpdate = false) => {\n  modalApi.setProps({\n    title: isUpdate ? 'common.edit' : 'common.add',\n    maxHeight: '90',\n    minWidth: '70',\n  })\n  modalApi.setContent(RulesetForm, { id, isUpdate })\n  modalApi.open()\n}\n\nconst handleUpdateRulesets = async () => {\n  try {\n    await rulesetsStore.updateRulesets()\n    message.success('common.success')\n  } catch (error: any) {\n    console.error('updateRulesets: ', error)\n    message.error(error)\n  }\n}\n\nconst handleEditRulesetList = (id: string) => {\n  modalApi.setProps({\n    title: rulesetsStore.getRulesetById(id)?.tag,\n    height: '90',\n    width: '90',\n  })\n  modalApi.setContent(RulesetView, { id })\n  modalApi.open()\n}\n\nconst handleUpdateRuleset = async (r: RuleSet) => {\n  try {\n    await rulesetsStore.updateRuleset(r.id)\n  } catch (error: any) {\n    console.error('updateRuleset: ', error)\n    message.error(error)\n  }\n}\n\nconst handleDeleteRuleset = async (r: RuleSet) => {\n  try {\n    await ignoredError(RemoveFile, r.path)\n    await rulesetsStore.deleteRuleset(r.id)\n  } catch (error: any) {\n    console.error('deleteRuleset: ', error)\n    message.error(error)\n  }\n}\n\nconst handleDisableRuleset = async (r: RuleSet) => {\n  r.disabled = !r.disabled\n  rulesetsStore.editRuleset(r.id, r)\n}\n\nconst handleClearRuleset = async (id: string) => {\n  const r = rulesetsStore.getRulesetById(id)\n  if (!r) return\n  if (r.format != RulesetFormat.Source) return\n\n  try {\n    await WriteFile(r.path, JSON.stringify(EmptyRuleSet, null, 2))\n    rulesetsStore.editRuleset(r.id, r)\n  } catch (error: any) {\n    message.error(error)\n    console.error(error)\n  }\n}\n\nconst generateMenus = (r: RuleSet) => {\n  return {\n    [RulesetFormat.Source]: sourceMenuList,\n    [RulesetFormat.Binary]: binaryMenuList,\n  }[r.format].map((v) => ({ ...v, handler: () => v.handler?.(r.id) }))\n}\n\nconst noUpdateNeeded = computed(() => rulesetsStore.rulesets.every((v) => v.disabled))\n\nconst onSortUpdate = debounce(rulesetsStore.saveRulesets, 1000)\n</script>\n\n<template>\n  <div v-if=\"rulesetsStore.rulesets.length === 0\" class=\"grid-list-empty\">\n    <Empty>\n      <template #description>\n        <I18nT keypath=\"rulesets.empty\" tag=\"div\" scope=\"global\" class=\"flex items-center mt-12\">\n          <template #action>\n            <Button type=\"link\" @click=\"handleShowRulesetForm()\">{{ t('common.add') }}</Button>\n          </template>\n          <template #import>\n            <Button type=\"link\" @click=\"handleImportRuleset\">{{ t('rulesets.hub') }}</Button>\n          </template>\n        </I18nT>\n      </template>\n    </Empty>\n  </div>\n\n  <div v-else class=\"grid-list-header\">\n    <Radio v-model=\"appSettingsStore.app.rulesetsView\" :options=\"ViewOptions\" class=\"mr-auto\" />\n    <Button type=\"link\" @click=\"handleImportRuleset\">\n      {{ t('rulesets.hub') }}\n    </Button>\n    <Button\n      :disabled=\"noUpdateNeeded\"\n      :type=\"noUpdateNeeded ? 'text' : 'link'\"\n      @click=\"handleUpdateRulesets\"\n    >\n      {{ t('common.updateAll') }}\n    </Button>\n    <Button type=\"primary\" icon=\"add\" class=\"ml-16\" @click=\"handleShowRulesetForm()\">\n      {{ t('common.add') }}\n    </Button>\n  </div>\n\n  <div\n    v-draggable=\"[rulesetsStore.rulesets, { ...DraggableOptions, onUpdate: onSortUpdate }]\"\n    :class=\"'grid-list-' + appSettingsStore.app.rulesetsView\"\n  >\n    <Card\n      v-for=\"r in rulesetsStore.rulesets\"\n      :key=\"r.id\"\n      v-menu=\"generateMenus(r)\"\n      :title=\"r.tag\"\n      :disabled=\"r.disabled\"\n      class=\"grid-list-item\"\n    >\n      <template #title-prefix>\n        <Tag v-if=\"r.updating\" color=\"cyan\" size=\"small\">\n          {{ t('ruleset.updating') }}\n        </Tag>\n      </template>\n\n      <template v-if=\"appSettingsStore.app.rulesetsView === View.Grid\" #extra>\n        <Dropdown>\n          <Button type=\"link\" size=\"small\" icon=\"more\" />\n          <template #overlay>\n            <div class=\"flex flex-col gap-4 min-w-64 p-4\">\n              <Button\n                :disabled=\"r.disabled\"\n                :loading=\"r.updating\"\n                :type=\"r.disabled ? 'text' : 'text'\"\n                @click=\"handleUpdateRuleset(r)\"\n              >\n                {{ t('common.update') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleDisableRuleset(r)\">\n                {{ r.disabled ? t('common.enable') : t('common.disable') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleShowRulesetForm(r.id, true)\">\n                {{ t('common.edit') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleDeleteRuleset(r)\">\n                {{ t('common.delete') }}\n              </Button>\n            </div>\n          </template>\n        </Dropdown>\n      </template>\n\n      <template v-else #extra>\n        <Button\n          :disabled=\"r.disabled\"\n          :loading=\"r.updating\"\n          :type=\"r.disabled ? 'text' : 'text'\"\n          size=\"small\"\n          @click=\"handleUpdateRuleset(r)\"\n        >\n          {{ t('common.update') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" @click=\"handleDisableRuleset(r)\">\n          {{ r.disabled ? t('common.enable') : t('common.disable') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" @click=\"handleShowRulesetForm(r.id, true)\">\n          {{ t('common.edit') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" @click=\"handleDeleteRuleset(r)\">\n          {{ t('common.delete') }}\n        </Button>\n      </template>\n\n      <div v-if=\"r.format === RulesetFormat.Binary\">\n        {{ t('ruleset.format.name') }}\n        :\n        {{ r.format || '--' }}\n      </div>\n\n      <template v-if=\"appSettingsStore.app.rulesetsView === View.Grid\">\n        <div v-if=\"r.format === RulesetFormat.Source\">\n          {{ t('rulesets.rulesetCount') }}\n          :\n          {{ r.count }}\n        </div>\n        <div>\n          {{ t('common.updateTime') }}\n          :\n          {{ r.updateTime ? formatRelativeTime(r.updateTime) : '--' }}\n        </div>\n      </template>\n      <template v-else>\n        <div v-if=\"r.format === RulesetFormat.Source\">\n          {{ t('rulesets.rulesetCount') }}\n          :\n          {{ r.count }}\n        </div>\n        <div>\n          {{ t('common.updateTime') }}\n          :\n          {{ r.updateTime ? formatDate(r.updateTime, 'YYYY-MM-DD HH:mm:ss') : '--' }}\n        </div>\n      </template>\n    </Card>\n  </div>\n\n  <Modal />\n</template>\n"
  },
  {
    "path": "frontend/src/views/ScheduledTasksView/components/ScheduledTaskForm.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, h } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { ScheduledTaskOptions } from '@/constant/app'\nimport { ScheduledTasksType } from '@/enums/app'\nimport {\n  useScheduledTasksStore,\n  useSubscribesStore,\n  useRulesetsStore,\n  usePluginsStore,\n} from '@/stores'\nimport { alert, deepClone, formatDate, isValidCron, message, sampleID } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\nimport type { ScheduledTask } from '@/types/app'\n\ninterface Props {\n  id?: string\n}\n\nconst props = defineProps<Props>()\n\nconst loading = ref(false)\n\nconst task = ref<ScheduledTask>({\n  id: sampleID(),\n  name: '',\n  type: ScheduledTasksType.RunScript,\n  subscriptions: [],\n  rulesets: [],\n  plugins: [],\n  script: '',\n  cron: '',\n  notification: false,\n  disabled: false,\n  lastTime: 0,\n})\n\nconst { t } = useI18n()\nconst scheduledTasksStore = useScheduledTasksStore()\nconst subscribesStore = useSubscribesStore()\nconst rulesetsStore = useRulesetsStore()\nconst pluginsStore = usePluginsStore()\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst handleSave = async () => {\n  const { ok, reason } = isValidCron(task.value.cron)\n  if (!ok) {\n    message.error(reason)\n    return\n  }\n\n  switch (task.value.type) {\n    case ScheduledTasksType.UpdateSubscription:\n      task.value.subscriptions = task.value.subscriptions.filter((id) =>\n        subscribesStore.getSubscribeById(id),\n      )\n      break\n    case ScheduledTasksType.UpdateRuleset:\n      task.value.rulesets = task.value.rulesets.filter((id) => rulesetsStore.getRulesetById(id))\n      break\n    case ScheduledTasksType.UpdatePlugin:\n    case ScheduledTasksType.RunPlugin:\n      task.value.plugins = task.value.plugins.filter((id) => pluginsStore.getPluginById(id))\n      break\n  }\n\n  loading.value = true\n\n  try {\n    if (props.id) {\n      await scheduledTasksStore.editScheduledTask(props.id, task.value)\n    } else {\n      await scheduledTasksStore.addScheduledTask(task.value)\n    }\n    await handleSubmit()\n  } catch (error: any) {\n    console.error(error)\n    message.error(error)\n  }\n\n  loading.value = false\n}\n\nconst handleUse = (list: string[], id: string) => {\n  const idx = list.findIndex((v) => v === id)\n  if (idx !== -1) {\n    list.splice(idx, 1)\n  } else {\n    list.push(id)\n  }\n}\n\nconst handleValidate = () => {\n  const { ok, reason } = isValidCron(task.value.cron)\n  if (!ok) {\n    message.error(reason)\n    return\n  }\n  message.success('common.success')\n}\n\nconst handleViewNextRuns = () => {\n  const { ok, reason, instance } = isValidCron(task.value.cron)\n  if (!ok) {\n    message.error(reason)\n    return\n  }\n  const list = instance!.nextRuns(99).map((v, i) => {\n    const index = (i + 1).toString().padStart(2, '0')\n    return index + ' - '.repeat(14) + formatDate(v.getTime(), 'YYYY/MM/DD HH:mm:ss')\n  })\n  alert('Next Run Time', list.join('\\n'))\n}\n\nif (props.id) {\n  const s = scheduledTasksStore.getScheduledTaskById(props.id)\n  if (s) {\n    task.value = deepClone(s)\n  }\n}\n\nconst modalSlots = {\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        disabled: !task.value.name || !task.value.cron,\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div>\n    <div class=\"form-item\">\n      {{ t('scheduledtask.name') }} *\n      <div class=\"min-w-[75%]\">\n        <Input v-model=\"task.name\" autofocus class=\"w-full\" />\n      </div>\n    </div>\n    <div class=\"form-item\">\n      {{ t('scheduledtask.cron') }} *\n      <div class=\"min-w-[75%]\">\n        <Input v-model=\"task.cron\" :placeholder=\"t('scheduledtask.cronTips')\" class=\"w-full\">\n          <template #suffix>\n            <Button type=\"primary\" size=\"small\" @click=\"handleValidate\">Validate</Button>\n            <Button type=\"primary\" size=\"small\" class=\"ml-4\" @click=\"handleViewNextRuns\">\n              Next Run Time\n            </Button>\n          </template>\n        </Input>\n      </div>\n    </div>\n    <div class=\"form-item\">\n      <div>{{ t('scheduledtask.type') }}</div>\n      <Radio v-model=\"task.type\" :options=\"ScheduledTaskOptions.slice(5)\" />\n    </div>\n    <div class=\"form-item\">\n      <div></div>\n      <Radio v-model=\"task.type\" :options=\"ScheduledTaskOptions.slice(0, 5)\" />\n    </div>\n    <div class=\"form-item\">\n      {{ t('scheduledtask.notification') }}\n      <Switch v-model=\"task.notification\" />\n    </div>\n\n    <div v-if=\"task.type === ScheduledTasksType.UpdateSubscription\">\n      <Divider>{{ t('scheduledtask.subscriptions') }}</Divider>\n      <Empty v-if=\"subscribesStore.subscribes.length === 0\" />\n      <div class=\"grid grid-cols-3 gap-8\">\n        <Card\n          v-for=\"s in subscribesStore.subscribes\"\n          :key=\"s.id\"\n          :title=\"s.name\"\n          :selected=\"task.subscriptions.includes(s.id)\"\n          @click=\"handleUse(task.subscriptions, s.id)\"\n        >\n          <div class=\"text-12 line-clamp-2\">{{ s.type }}</div>\n        </Card>\n      </div>\n    </div>\n\n    <div v-else-if=\"task.type === ScheduledTasksType.UpdateRuleset\">\n      <Divider>{{ t('scheduledtask.rulesets') }}</Divider>\n      <Empty v-if=\"rulesetsStore.rulesets.length === 0\" />\n      <div class=\"grid grid-cols-3 gap-8\">\n        <Card\n          v-for=\"r in rulesetsStore.rulesets\"\n          :key=\"r.id\"\n          :title=\"r.tag\"\n          :selected=\"task.rulesets.includes(r.id)\"\n          @click=\"handleUse(task.rulesets, r.id)\"\n        >\n          <div class=\"text-12 line-clamp-2\">{{ r.type }}</div>\n        </Card>\n      </div>\n    </div>\n\n    <div v-else-if=\"task.type === ScheduledTasksType.UpdatePlugin\">\n      <Divider>{{ t('scheduledtask.plugins') }}</Divider>\n      <Empty v-if=\"pluginsStore.plugins.length === 0\" />\n      <div class=\"grid grid-cols-3 gap-8\">\n        <Card\n          v-for=\"p in pluginsStore.plugins\"\n          :key=\"p.id\"\n          :title=\"p.name\"\n          :selected=\"task.plugins.includes(p.id)\"\n          @click=\"handleUse(task.plugins, p.id)\"\n        >\n          <div class=\"text-12 line-clamp-2\">{{ p.type }}</div>\n        </Card>\n      </div>\n    </div>\n\n    <div v-else-if=\"task.type === ScheduledTasksType.RunPlugin\">\n      <Divider>{{ t('scheduledtask.plugins') }}</Divider>\n      <Empty v-if=\"pluginsStore.plugins.length === 0\" />\n      <div class=\"grid grid-cols-3 gap-8\">\n        <Card\n          v-for=\"p in pluginsStore.plugins\"\n          :key=\"p.id\"\n          v-tips=\"p.description\"\n          :title=\"p.name\"\n          :selected=\"task.plugins.includes(p.id)\"\n          @click=\"handleUse(task.plugins, p.id)\"\n        >\n          <div class=\"text-12 line-clamp-2\">{{ p.description }}</div>\n        </Card>\n      </div>\n    </div>\n\n    <div v-else-if=\"task.type === ScheduledTasksType.RunScript\">\n      <Divider>{{ t('scheduledtask.script') }}</Divider>\n      <CodeViewer v-model=\"task.script\" editable lang=\"javascript\" />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ScheduledTasksView/components/ScheduledTasksLogs.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useLogsStore, useScheduledTasksStore } from '@/stores'\nimport { buildSmartRegExp, formatDate } from '@/utils'\n\nimport type { Column } from '@/components/Table/index.vue'\n\ninterface Props {\n  id?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), { id: '' })\n\nconst { t } = useI18n()\nconst logsStore = useLogsStore()\nconst pluginsStore = useScheduledTasksStore()\n\nconst plugin = ref(pluginsStore.getScheduledTaskById(props.id)?.name)\nconst keywords = ref('')\n\nconst columns: Column[] = [\n  {\n    title: 'scheduledtasks.name',\n    align: 'center',\n    key: 'name',\n  },\n  {\n    title: 'scheduledtasks.startTime',\n    align: 'center',\n    key: 'startTime',\n    customRender: ({ value }) => formatDate(value, 'YYYY-MM-DD HH:mm:ss'),\n  },\n  {\n    title: 'scheduledtasks.endTime',\n    align: 'center',\n    key: 'endTime',\n    customRender: ({ value }) => formatDate(value, 'YYYY-MM-DD HH:mm:ss'),\n  },\n  {\n    title: 'scheduledtasks.duration',\n    align: 'center',\n    key: 'endTime',\n    sort: (a, b) => b.endTime - b.startTime - (a.endTime - a.startTime),\n    customRender: ({ value, record }) => {\n      return ((value - record.startTime) / 1000).toFixed(2) + 's'\n    },\n  },\n  {\n    title: 'scheduledtasks.result',\n    align: 'center',\n    key: 'result',\n  },\n]\n\nconst pluginsOptions = computed(() =>\n  [{ label: 'All', value: '' }].concat(\n    ...pluginsStore.scheduledtasks.map((v) => ({\n      label: v.name,\n      value: v.name,\n    })),\n  ),\n)\n\nconst filteredLogs = computed(() => {\n  return logsStore.scheduledtasksLogs.filter((v) => {\n    const p = plugin.value ? v.name === plugin.value : true\n    const k = buildSmartRegExp(keywords.value, 'i').test(JSON.stringify(v.result))\n    return p && k\n  })\n})\n\nconst clearLogs = () => logsStore.scheduledtasksLogs.splice(0)\n</script>\n\n<template>\n  <div class=\"h-full flex flex-col\">\n    <div class=\"flex items-center\">\n      <span class=\"mr-4\">\n        {{ t('scheduledtasks.name') }}\n        :\n      </span>\n      <Select v-model=\"plugin\" :options=\"pluginsOptions\" size=\"small\" />\n      <Input\n        v-model=\"keywords\"\n        clearable\n        size=\"small\"\n        :placeholder=\"t('common.keywords')\"\n        class=\"ml-8 flex-1\"\n      />\n      <Button\n        v-tips=\"'common.clear'\"\n        icon=\"clear\"\n        size=\"small\"\n        type=\"text\"\n        class=\"ml-8\"\n        @click=\"clearLogs\"\n      />\n    </div>\n\n    <Empty v-if=\"filteredLogs.length === 0\" />\n\n    <Table v-else :columns=\"columns\" :data-source=\"filteredLogs\" sort=\"start\" class=\"mt-8\">\n      <template #result=\"{ record }\">\n        <div class=\"flex flex-col gap-8 text-left\">\n          <div v-for=\"item in record.result\" :key=\"item\">\n            <span :style=\"{ color: item.ok ? 'greenyellow' : 'red' }\">●</span>\n            {{ item.result }}\n          </div>\n        </div>\n      </template>\n    </Table>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/ScheduledTasksView/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { Cron } from 'croner'\nimport { defineAsyncComponent } from 'vue'\nimport { useI18n, I18nT } from 'vue-i18n'\n\nimport { DraggableOptions, ViewOptions } from '@/constant/app'\nimport { View } from '@/enums/app'\nimport { useAppSettingsStore, useScheduledTasksStore } from '@/stores'\nimport { debounce, formatRelativeTime, formatDate, message, alert } from '@/utils'\n\nimport { useModal } from '@/components/Modal'\n\nimport type { Menu, ScheduledTask } from '@/types/app'\n\nconst ScheduledTaskForm = defineAsyncComponent(() => import('./components/ScheduledTaskForm.vue'))\nconst ScheduledTasksLogs = defineAsyncComponent(() => import('./components/ScheduledTasksLogs.vue'))\n\nconst menuList: Menu[] = [\n  {\n    label: 'scheduledtasks.run',\n    handler: (id: string) => {\n      scheduledTasksStore.runScheduledTask(id)\n    },\n  },\n  {\n    label: 'scheduledtasks.next',\n    handler: (id: string) => {\n      const task = scheduledTasksStore.getScheduledTaskById(id)\n      if (task) {\n        const list = new Cron(task.cron).nextRuns(99).map((v, i) => {\n          const index = (i + 1).toString().padStart(2, '0')\n          return index + ' - '.repeat(14) + formatDate(v.getTime(), 'YYYY/MM/DD HH:mm:ss')\n        })\n        alert('Next Run Time', list.join('\\n'))\n      }\n    },\n  },\n  {\n    label: 'scheduledtasks.log',\n    handler: (id: string) => {\n      handleShowTaskLogs(id)\n    },\n  },\n]\n\nconst { t } = useI18n()\nconst [Modal, modalApi] = useModal({})\nconst scheduledTasksStore = useScheduledTasksStore()\nconst appSettingsStore = useAppSettingsStore()\n\nconst handleShowTaskLogs = (id?: string) => {\n  modalApi.setProps({\n    title: 'scheduledtasks.logs',\n    cancelText: 'common.close',\n    maskClosable: true,\n    submit: false,\n    width: '90',\n    height: '90',\n  })\n  modalApi.setContent(ScheduledTasksLogs, { id }).open()\n}\n\nconst handleShowTaskForm = (id?: string) => {\n  modalApi.setProps({\n    title: id ? 'common.edit' : 'common.add',\n    maxHeight: '90',\n    minWidth: '70',\n    maxWidth: '90',\n  })\n  modalApi.setContent(ScheduledTaskForm, { id }).open()\n}\n\nconst handleDeleteTask = async (s: ScheduledTask) => {\n  try {\n    await scheduledTasksStore.deleteScheduledTask(s.id)\n  } catch (error: any) {\n    console.error('deleteSubscribe: ', error)\n    message.error(error)\n  }\n}\n\nconst handleDisableTask = async (s: ScheduledTask) => {\n  s.disabled = !s.disabled\n  scheduledTasksStore.editScheduledTask(s.id, s)\n}\n\nconst onSortUpdate = debounce(scheduledTasksStore.saveScheduledTasks, 1000)\n</script>\n\n<template>\n  <div v-if=\"scheduledTasksStore.scheduledtasks.length === 0\" class=\"grid-list-empty\">\n    <Empty>\n      <template #description>\n        <I18nT\n          keypath=\"scheduledtasks.empty\"\n          tag=\"div\"\n          scope=\"global\"\n          class=\"flex items-center mt-12\"\n        >\n          <template #action>\n            <Button type=\"link\" @click=\"handleShowTaskForm()\">{{ t('common.add') }}</Button>\n          </template>\n        </I18nT>\n      </template>\n    </Empty>\n  </div>\n\n  <div v-else class=\"grid-list-header\">\n    <Radio\n      v-model=\"appSettingsStore.app.scheduledtasksView\"\n      :options=\"ViewOptions\"\n      class=\"mr-auto\"\n    />\n    <Button type=\"text\" @click=\"handleShowTaskLogs()\">\n      {{ t('scheduledtasks.logs') }}\n    </Button>\n    <Button type=\"primary\" icon=\"add\" class=\"ml-16\" @click=\"handleShowTaskForm()\">\n      {{ t('common.add') }}\n    </Button>\n  </div>\n\n  <div\n    v-draggable=\"[\n      scheduledTasksStore.scheduledtasks,\n      { ...DraggableOptions, onUpdate: onSortUpdate },\n    ]\"\n    :class=\"'grid-list-' + appSettingsStore.app.scheduledtasksView\"\n  >\n    <Card\n      v-for=\"s in scheduledTasksStore.scheduledtasks\"\n      :key=\"s.id\"\n      v-menu=\"menuList.map((v) => ({ ...v, handler: () => v.handler?.(s.id) }))\"\n      :title=\"s.name\"\n      :disabled=\"s.disabled\"\n      class=\"grid-list-item\"\n    >\n      <template v-if=\"appSettingsStore.app.scheduledtasksView === View.Grid\" #extra>\n        <Dropdown>\n          <Button type=\"link\" size=\"small\" icon=\"more\" />\n          <template #overlay>\n            <div class=\"flex flex-col gap-4 min-w-64 p-4\">\n              <Button type=\"text\" @click=\"handleDisableTask(s)\">\n                {{ s.disabled ? t('common.enable') : t('common.disable') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleShowTaskForm(s.id)\">\n                {{ t('common.edit') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleDeleteTask(s)\">\n                {{ t('common.delete') }}\n              </Button>\n            </div>\n          </template>\n        </Dropdown>\n      </template>\n\n      <template v-else #extra>\n        <Button type=\"text\" size=\"small\" @click=\"handleDisableTask(s)\">\n          {{ s.disabled ? t('common.enable') : t('common.disable') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" @click=\"handleShowTaskForm(s.id)\">\n          {{ t('common.edit') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" @click=\"handleDeleteTask(s)\">\n          {{ t('common.delete') }}\n        </Button>\n      </template>\n      <div>\n        {{ t('scheduledtask.type') }}\n        :\n        {{ t('scheduledtask.' + s.type) }}\n      </div>\n      <div>\n        {{ t('scheduledtask.cron') }}\n        :\n        {{ s.cron }}\n      </div>\n      <div v-if=\"appSettingsStore.app.scheduledtasksView === View.Grid\">\n        {{ t('scheduledtask.lastTime') }}\n        :\n        {{ s.lastTime ? formatRelativeTime(s.lastTime) : '--' }}\n      </div>\n      <div v-else>\n        {{ t('scheduledtask.lastTime') }}\n        :\n        {{ s.lastTime ? formatDate(s.lastTime, 'YYYY-MM-DD HH:mm:ss') : '--' }}\n      </div>\n    </Card>\n  </div>\n\n  <Modal />\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/CoreSettings.vue",
    "content": "<script setup lang=\"ts\">\nimport { defineAsyncComponent } from 'vue'\n\nimport { useModal } from '@/components/Modal'\n\nconst BranchDetail = defineAsyncComponent(() => import('./components/BranchDetail.vue'))\nconst CoreConfiguration = defineAsyncComponent(() => import('./components/CoreConfig.vue'))\nconst SwitchBranch = defineAsyncComponent(() => import('./components/SwitchBranch.vue'))\n\nconst [ConfigModal, modalApi] = useModal({})\n\nconst handleCoreConfiguraion = async (isAlpha: boolean) => {\n  modalApi.setProps({ title: 'settings.kernel.config.name', minWidth: '70' })\n  modalApi.setContent(CoreConfiguration, { isAlpha }).open()\n}\n</script>\n\n<template>\n  <div>\n    <BranchDetail :is-alpha=\"false\" @config=\"handleCoreConfiguraion(false)\" />\n    <BranchDetail :is-alpha=\"true\" @config=\"handleCoreConfiguraion(true)\" />\n    <SwitchBranch />\n    <ConfigModal />\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/GeneralSettings.vue",
    "content": "<script setup lang=\"ts\">\nimport AdvancedSettings from './components/AdvancedSettings.vue'\nimport BehaviorSettings from './components/BehaviorSettings.vue'\nimport FeatureSettings from './components/FeatureSettings.vue'\nimport PersonalizationSettings from './components/PersonalizationSettings.vue'\nimport SystemProxySettings from './components/SystemProxySettings.vue'\n</script>\n\n<template>\n  <div class=\"flex flex-col gap-8 pr-20 mb-8\">\n    <PersonalizationSettings />\n    <BehaviorSettings />\n    <AdvancedSettings />\n    <SystemProxySettings />\n    <FeatureSettings />\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/PluginSettings.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue'\n\nimport { useAppSettingsStore, usePluginsStore } from '@/stores'\nimport PluginConfigItem from '@/views/PluginsView/components/PluginConfigItem.vue'\n\nconst appSettingsStore = useAppSettingsStore()\nconst pluginsStore = usePluginsStore()\n\nconst plugins = computed(() =>\n  pluginsStore.plugins.filter((plugin) => plugin.configuration.length > 0),\n)\n</script>\n\n<template>\n  <div class=\"flex flex-col gap-8 pr-12 mb-8\">\n    <template v-if=\"plugins.length === 0\">\n      <div class=\"px-8 py-12 text-18 font-bold\">\n        {{ $t('plugins.configuration') }}\n      </div>\n      <Card>\n        <div class=\"py-32\"><Empty /></div>\n      </Card>\n    </template>\n\n    <PluginConfigItem\n      v-for=\"plugin in plugins\"\n      :key=\"plugin.id\"\n      :plugin=\"plugin\"\n      :model-value=\"appSettingsStore.app.pluginSettings[plugin.id]\"\n      @change=\"\n        (val: Recordable) => {\n          if (JSON.stringify(val) === '{}') {\n            delete appSettingsStore.app.pluginSettings[plugin.id]\n          } else {\n            appSettingsStore.app.pluginSettings[plugin.id] = val\n          }\n        }\n      \"\n    >\n      <template #header=\"{ handleResetAll }\">\n        <div class=\"flex items-center px-8 py-12\">\n          <Dropdown>\n            <Button\n              icon=\"settings\"\n              type=\"text\"\n              size=\"small\"\n              class=\"mr-4\"\n              style=\"margin-left: -8px\"\n            />\n            <template #overlay=\"{ close }\">\n              <div class=\"flex flex-col gap-4 min-w-64 p-4\">\n                <Button\n                  type=\"text\"\n                  size=\"small\"\n                  @click=\"\n                    () => {\n                      handleResetAll()\n                      close()\n                    }\n                  \"\n                >\n                  {{ $t('settings.plugin.resetSettings') }}\n                </Button>\n              </div>\n            </template>\n          </Dropdown>\n          <div class=\"text-18 font-bold\">{{ plugin.name }}</div>\n        </div>\n      </template>\n    </PluginConfigItem>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/components/AdvancedSettings.vue",
    "content": "<script lang=\"ts\" setup>\nimport { MakeDir, OpenDir } from '@/bridge'\nimport { RollingReleaseDirectory } from '@/constant/app'\nimport { useAppSettingsStore, useEnvStore } from '@/stores'\nimport { APP_TITLE, APP_VERSION } from '@/utils'\n\nconst appSettings = useAppSettingsStore()\nconst envStore = useEnvStore()\n\nconst handleOpenFolder = async () => {\n  await OpenDir(envStore.env.basePath)\n}\n\nconst handleOpenRollingReleaseFolder = async () => {\n  await MakeDir(RollingReleaseDirectory)\n  await OpenDir(RollingReleaseDirectory)\n}\n\nconst handleClearApiToken = () => {\n  appSettings.app.githubApiToken = ''\n}\n\nconst handleClearUserAgent = () => {\n  appSettings.app.userAgent = ''\n}\n</script>\n\n<template>\n  <div class=\"px-8 py-12 text-18 font-bold\">{{ $t('settings.advanced') }}</div>\n\n  <Card>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.appFolder.name') }}</div>\n      <Button type=\"primary\" icon=\"folder\" @click=\"handleOpenFolder\">\n        <span class=\"ml-8\">{{ $t('settings.appFolder.open') }}</span>\n      </Button>\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">\n        {{ $t('settings.rollingRelease') }}\n        <span class=\"font-normal text-12\">({{ $t('settings.needRestart') }})</span>\n      </div>\n      <div class=\"flex items-center gap-4\">\n        <Button type=\"primary\" icon=\"folder\" size=\"small\" @click=\"handleOpenRollingReleaseFolder\" />\n        <Switch v-model=\"appSettings.app.rollingRelease\" />\n      </div>\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.realMemoryUsage') }}</div>\n      <Switch v-model=\"appSettings.app.kernel.realMemoryUsage\" />\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">\n        {{ $t('settings.autoRestartKernel.name') }}\n        <span class=\"font-normal text-12\">({{ $t('settings.autoRestartKernel.tips') }})</span>\n      </div>\n      <Switch v-model=\"appSettings.app.autoRestartKernel\" />\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">\n        {{ $t('settings.githubapi.name') }}\n        <span class=\"font-normal text-12\">({{ $t('settings.githubapi.tips') }})</span>\n      </div>\n      <Input v-model.lazy=\"appSettings.app.githubApiToken\" editable class=\"text-14\">\n        <template #suffix>\n          <Button\n            v-tips=\"'settings.userAgent.reset'\"\n            type=\"text\"\n            size=\"small\"\n            icon=\"reset\"\n            @click=\"handleClearApiToken\"\n          />\n        </template>\n      </Input>\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">\n        {{ $t('settings.userAgent.name') }}\n        <span class=\"font-normal text-12\">({{ $t('settings.userAgent.tips') }})</span>\n      </div>\n      <Input\n        v-model.lazy=\"appSettings.app.userAgent\"\n        :placeholder=\"APP_TITLE + '/' + APP_VERSION\"\n        editable\n        class=\"text-14\"\n      >\n        <template #suffix>\n          <Button\n            v-tips=\"'settings.userAgent.reset'\"\n            type=\"text\"\n            size=\"small\"\n            icon=\"reset\"\n            @click=\"handleClearUserAgent\"\n          />\n        </template>\n      </Input>\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">\n        {{ $t('settings.multipleInstance') }}\n        <span class=\"font-normal text-12\">({{ $t('settings.needRestart') }})</span>\n      </div>\n      <Switch v-model=\"appSettings.app.multipleInstance\" />\n    </div>\n  </Card>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/components/BehaviorSettings.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref } from 'vue'\n\nimport { WriteFile, RemoveFile, AbsolutePath, ExitApp } from '@/bridge'\nimport { WebviewGpuPolicyOptions, WindowStateOptions } from '@/constant/app'\nimport { useAppSettingsStore, useEnvStore } from '@/stores'\nimport {\n  APP_TITLE,\n  getTaskSchXmlString,\n  confirm,\n  message,\n  QuerySchTask,\n  CreateSchTask,\n  DeleteSchTask,\n  CheckPermissions,\n  SwitchPermissions,\n  RunWithPowerShell,\n} from '@/utils'\n\nconst appSettings = useAppSettingsStore()\nconst envStore = useEnvStore()\n\nconst isAdmin = ref(false)\nconst isTaskScheduled = ref(false)\n\nconst restartApp = async (admin = false) => {\n  if (admin) {\n    await RunWithPowerShell(envStore.env.appPath, [], { admin, wait: false })\n  } else {\n    await RunWithPowerShell('explorer', [envStore.env.appPath], { wait: false })\n  }\n  await ExitApp()\n}\n\nconst onPermChange = async (v: boolean) => {\n  try {\n    await SwitchPermissions(v)\n    if (v !== envStore.env.isPrivileged) {\n      const ok = await confirm('Notice', 'Restart the application now?').catch(() => 0)\n      ok && (await restartApp(v))\n    }\n  } catch (error: any) {\n    message.error(error)\n    console.log(error)\n  }\n}\n\nconst onTaskSchChange = async (v: boolean) => {\n  isTaskScheduled.value = !v\n  try {\n    if (v) {\n      await createSchTask(appSettings.app.startupDelay)\n    } else {\n      await DeleteSchTask(APP_TITLE)\n    }\n    isTaskScheduled.value = v\n  } catch (error: any) {\n    console.error(error)\n    message.error(error)\n  }\n}\n\nconst onStartupDelayChange = async (delay: number) => {\n  if (appSettings.app.startupDelay !== delay) {\n    try {\n      await createSchTask(delay)\n      appSettings.app.startupDelay = delay\n    } catch (error: any) {\n      console.error(error)\n      message.error(error)\n    }\n  }\n}\n\nconst checkSchtask = async () => {\n  try {\n    await QuerySchTask(APP_TITLE)\n    isTaskScheduled.value = true\n  } catch {\n    isTaskScheduled.value = false\n  }\n}\n\nconst createSchTask = async (delay = 30) => {\n  const xmlPath = 'data/.cache/tasksch.xml'\n  const xmlContent = await getTaskSchXmlString(delay)\n  await WriteFile(xmlPath, xmlContent)\n  await CreateSchTask(APP_TITLE, await AbsolutePath(xmlPath))\n  await RemoveFile(xmlPath)\n}\n\nif (envStore.env.os === 'windows') {\n  checkSchtask()\n\n  CheckPermissions().then((admin) => {\n    isAdmin.value = admin\n  })\n}\n</script>\n\n<template>\n  <div class=\"px-8 py-12 text-18 font-bold\">{{ $t('settings.behavior') }}</div>\n\n  <Card>\n    <div v-platform=\"['windows']\" class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">\n        {{ $t('settings.admin') }}\n        <span class=\"font-normal text-12\">({{ $t('settings.needRestart') }})</span>\n      </div>\n      <div class=\"flex items-center gap-4\">\n        <Button\n          v-if=\"envStore.env.isPrivileged !== isAdmin\"\n          v-tips=\"'titlebar.restart'\"\n          type=\"primary\"\n          icon=\"refresh\"\n          size=\"small\"\n          @click=\"() => restartApp(isAdmin)\"\n        />\n        <Switch v-model=\"isAdmin\" @change=\"onPermChange\" />\n      </div>\n    </div>\n    <div v-platform=\"['windows']\" class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">\n        {{ $t('settings.startup.name') }}\n        <span class=\"font-normal text-12\">({{ $t('settings.needAdmin') }})</span>\n      </div>\n      <div class=\"flex items-center\">\n        <Radio\n          v-if=\"isTaskScheduled\"\n          v-model=\"appSettings.app.windowStartState\"\n          :options=\"WindowStateOptions\"\n          type=\"number\"\n        />\n        <Switch v-model=\"isTaskScheduled\" class=\"ml-16\" @change=\"onTaskSchChange\" />\n      </div>\n    </div>\n    <div\n      v-if=\"isTaskScheduled\"\n      v-platform=\"['windows']\"\n      class=\"px-8 py-12 flex items-center justify-between\"\n    >\n      <div class=\"text-16 font-bold\">\n        {{ $t('settings.startup.startupDelay') }}\n        <span class=\"font-normal text-12\">({{ $t('settings.needAdmin') }})</span>\n      </div>\n      <Input\n        :model-value=\"appSettings.app.startupDelay\"\n        :min=\"10\"\n        :max=\"180\"\n        editable\n        type=\"number\"\n        @submit=\"onStartupDelayChange\"\n      >\n        <template #suffix=\"{ showInput }\">\n          <span class=\"ml-4\" @click=\"showInput\">{{ $t('settings.startup.delay') }}</span>\n        </template>\n      </Input>\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.exitOnClose') }}</div>\n      <Switch v-model=\"appSettings.app.exitOnClose\" />\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.autoStartKernel') }}</div>\n      <Switch v-model=\"appSettings.app.autoStartKernel\" />\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.closeKernelOnExit') }}</div>\n      <Switch v-model=\"appSettings.app.closeKernelOnExit\" />\n    </div>\n    <div v-platform=\"['linux']\" class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">\n        {{ $t('settings.webviewGpuPolicy.name') }}\n        <span class=\"font-normal text-12\">({{ $t('settings.needRestart') }})</span>\n      </div>\n      <Radio v-model=\"appSettings.app.webviewGpuPolicy\" :options=\"WebviewGpuPolicyOptions\" />\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.addPluginToMenu') }}</div>\n      <Switch v-model=\"appSettings.app.addPluginToMenu\" />\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.addGroupToMenu') }}</div>\n      <Switch v-model=\"appSettings.app.addGroupToMenu\" />\n    </div>\n  </Card>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/components/BranchDetail.vue",
    "content": "<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n'\n\nimport { RemoveFile } from '@/bridge'\nimport { CoreCacheFilePath } from '@/constant/kernel'\nimport { useCoreBranch } from '@/hooks/useCoreBranch'\nimport { useKernelApiStore } from '@/stores'\nimport { message } from '@/utils'\n\ninterface Props {\n  isAlpha: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {})\n\nconst emit = defineEmits(['config'])\n\nconst { t } = useI18n()\nconst kernelApiStore = useKernelApiStore()\n\nconst {\n  restartable,\n  updatable,\n  grantable,\n  rollbackable,\n  versionDetail,\n  localVersion,\n  localVersionLoading,\n  remoteVersion,\n  remoteVersionLoading,\n  downloading,\n  refreshLocalVersion,\n  refreshRemoteVersion,\n  downloadCore,\n  restartCore,\n  rollbackCore,\n  grantCorePermission,\n  openReleasePage,\n  openFileLocation,\n} = useCoreBranch(props.isAlpha)\n\nconst handleClearCoreCache = async () => {\n  try {\n    if (kernelApiStore.running) {\n      await kernelApiStore.restartCore(() => RemoveFile(CoreCacheFilePath))\n    } else {\n      await RemoveFile(CoreCacheFilePath)\n    }\n    message.success('common.success')\n  } catch (error: any) {\n    message.error(error)\n    console.log(error)\n  }\n}\n</script>\n\n<template>\n  <div class=\"flex items-center px-4 my-12\">\n    <div class=\"mr-8 font-bold text-16\">\n      {{ isAlpha ? 'Alpha' : t('settings.kernel.name') }}\n    </div>\n    <Button\n      v-if=\"rollbackable\"\n      v-tips=\"'settings.kernel.rollbackTip'\"\n      icon=\"rollback\"\n      type=\"text\"\n      size=\"small\"\n      @click=\"rollbackCore\"\n    />\n    <Button\n      v-tips=\"'settings.kernel.clearCache'\"\n      type=\"text\"\n      size=\"small\"\n      icon=\"clear3\"\n      @click=\"handleClearCoreCache\"\n    />\n    <Button\n      v-if=\"grantable\"\n      v-tips=\"'settings.kernel.grant'\"\n      type=\"text\"\n      size=\"small\"\n      icon=\"grant\"\n      @click=\"grantCorePermission\"\n    />\n    <Button\n      v-tips=\"'settings.kernel.linkTip'\"\n      icon=\"link\"\n      type=\"text\"\n      size=\"small\"\n      @click=\"openReleasePage\"\n    />\n    <Button\n      v-tips=\"'settings.kernel.openTip'\"\n      icon=\"folder\"\n      type=\"text\"\n      size=\"small\"\n      @click=\"openFileLocation\"\n    />\n    <Button\n      v-tips=\"'settings.kernel.config.name'\"\n      type=\"text\"\n      size=\"small\"\n      icon=\"settings3\"\n      @click=\"emit('config')\"\n    />\n  </div>\n  <div class=\"flex items-center py-8 min-h-42\">\n    <Tag class=\"cursor-pointer\" @click=\"refreshLocalVersion(true)\">\n      {{ t('settings.kernel.local') }}\n      :\n      {{ localVersionLoading ? 'Loading' : localVersion || t('kernel.notFound') }}\n    </Tag>\n    <Tag class=\"cursor-pointer\" @click=\"refreshRemoteVersion(true)\">\n      {{ t('settings.kernel.remote') }}\n      :\n      {{ remoteVersionLoading ? 'Loading' : remoteVersion }}\n    </Tag>\n    <Button\n      v-show=\"!localVersionLoading && !remoteVersionLoading && updatable\"\n      :loading=\"downloading\"\n      size=\"small\"\n      type=\"primary\"\n      @click=\"downloadCore\"\n    >\n      {{ t('settings.kernel.update') }} : {{ remoteVersion }}\n    </Button>\n    <Button\n      v-show=\"!localVersionLoading && !remoteVersionLoading && restartable\"\n      :loading=\"kernelApiStore.restarting\"\n      size=\"small\"\n      type=\"primary\"\n      @click=\"restartCore\"\n    >\n      {{ t('settings.kernel.restart') }}\n    </Button>\n  </div>\n  <div class=\"text-12 px-4 py-8 break-all\">\n    {{ versionDetail }}\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/components/CoreConfig.vue",
    "content": "<script lang=\"ts\" setup>\nimport { h, inject, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { DraggableOptions } from '@/constant/app'\nimport { DefaultCoreConfig } from '@/constant/kernel'\nimport { useAppSettingsStore } from '@/stores'\nimport { deepClone, message, processMagicVariables } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\ninterface Props {\n  isAlpha: boolean\n}\n\nconst props = defineProps<Props>()\n\nconst tabs = [\n  { tab: 'settings.kernel.config.env', key: 'env' },\n  { tab: 'settings.kernel.config.args', key: 'args' },\n]\n\nconst activeKey = ref('env')\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst { t } = useI18n()\nconst appSettings = useAppSettingsStore()\n\nconst source = props.isAlpha ? appSettings.app.kernel.alpha : appSettings.app.kernel.main\n\nconst model = ref(deepClone(source))\n\nconst handleSave = () => {\n  Object.assign(source, model.value)\n  handleSubmit()\n}\n\nconst modalSlots = {\n  action: () =>\n    h(\n      Button,\n      {\n        type: 'link',\n        class: 'mr-auto',\n        onClick: () => {\n          const { env, args } = DefaultCoreConfig()\n          model.value.env = env\n          model.value.args = args\n          message.success('common.success')\n        },\n      },\n      () => t('plugin.restore'),\n    ),\n  cancel: () =>\n    h(\n      Button,\n      {\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div>\n    <Tabs v-model:active-key=\"activeKey\" :items=\"tabs\">\n      <template #env>\n        <Empty v-if=\"Object.keys(model.env).length === 0\" />\n        <Card v-else :title=\"t('common.preview')\">\n          <div class=\"flex flex-col gap-4\">\n            <div\n              v-for=\"[key, value] in Object.entries(model.env)\"\n              :key=\"key\"\n              class=\"flex items-center\"\n            >\n              <Tag class=\"w-[25%] self-stretch\">\n                <div\n                  class=\"h-full flex items-center justify-center py-2 break-all whitespace-pre-wrap\"\n                >\n                  {{ key }}\n                </div>\n              </Tag>\n              :\n              <Tag class=\"w-[75%] self-stretch\">\n                <div\n                  class=\"h-full flex items-center justify-center py-2 break-all whitespace-pre-wrap\"\n                >\n                  {{ processMagicVariables(value) }}\n                </div>\n              </Tag>\n            </div>\n          </div>\n        </Card>\n        <KeyValueEditor v-model=\"model.env\" class=\"mt-16\" />\n      </template>\n\n      <template #args>\n        <Empty v-if=\"model.args.length === 0\" />\n        <Card v-else :title=\"t('common.preview')\">\n          <div v-draggable=\"[model.args, DraggableOptions]\" class=\"flex flex-wrap items-center\">\n            <div v-for=\"item in model.args\" :key=\"item\">\n              <Tag size=\"small\">\n                <div class=\"py-2 break-all whitespace-pre-wrap\">\n                  {{ processMagicVariables(item) }}\n                </div>\n              </Tag>\n            </div>\n          </div>\n        </Card>\n        <InputList v-model=\"model.args\" class=\"mt-16\" />\n      </template>\n    </Tabs>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/components/FeatureSettings.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useAppSettingsStore } from '@/stores'\nconst appSettings = useAppSettingsStore()\n</script>\n\n<template>\n  <div class=\"px-8 py-12 text-18 font-bold\">{{ $t('settings.features') }}</div>\n\n  <Card>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.debugOutline') }}</div>\n      <Switch v-model=\"appSettings.app.debugOutline\" />\n    </div>\n    <div v-platform=\"['linux']\" class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.debugBorder') }}</div>\n      <Switch v-model=\"appSettings.app.debugBorder\" />\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.debugNoAnimation') }}</div>\n      <Switch v-model=\"appSettings.app.debugNoAnimation\" />\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.debugNoRounded') }}</div>\n      <Switch v-model=\"appSettings.app.debugNoRounded\" />\n    </div>\n  </Card>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/components/PersonalizationSettings.vue",
    "content": "<script lang=\"ts\" setup>\nimport { BrowserOpenURL, MakeDir, OpenDir } from '@/bridge'\nimport { ColorOptions, DefaultFontFamily, LocalesFilePath, ThemeOptions } from '@/constant/app'\nimport { Color } from '@/enums/app'\nimport routes from '@/router/routes'\nimport { useAppSettingsStore, useAppStore } from '@/stores'\nimport { APP_LOCALES_URL } from '@/utils'\n\nconst pages = routes.flatMap((route) => {\n  if (route.meta?.hidden !== undefined) return []\n  return {\n    label: route.meta!.name,\n    value: route.name as string,\n  }\n})\n\nconst appStore = useAppStore()\nconst appSettings = useAppSettingsStore()\n\nconst resetFontFamily = () => {\n  appSettings.app.fontFamily = DefaultFontFamily\n}\n\nconst onThemeClick = (e: MouseEvent) => {\n  document.documentElement.style.setProperty('--x', e.clientX + 'px')\n  document.documentElement.style.setProperty('--y', e.clientY + 'px')\n}\n\nconst handleOpenLocalesFolder = async () => {\n  await MakeDir(LocalesFilePath)\n  await OpenDir(LocalesFilePath)\n}\n</script>\n<template>\n  <div class=\"px-8 py-12 text-18 font-bold\">{{ $t('settings.personalization') }}</div>\n\n  <Card>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.theme.name') }}</div>\n      <Radio v-model=\"appSettings.app.theme\" :options=\"ThemeOptions\" @click=\"onThemeClick\" />\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.color.name') }}</div>\n      <div class=\"flex items-center\">\n        <div v-if=\"appSettings.app.color === Color.Custom\" class=\"flex items-center mr-4\">\n          <ColorPicker v-model=\"appSettings.app.primaryColor\">\n            <template #suffix>\n              <div class=\"text-12\">{{ $t('settings.color.primary') }}</div>\n            </template>\n          </ColorPicker>\n          <ColorPicker v-model=\"appSettings.app.secondaryColor\">\n            <template #suffix>\n              <div class=\"text-12\">{{ $t('settings.color.secondary') }}</div>\n            </template>\n          </ColorPicker>\n        </div>\n        <Radio v-model=\"appSettings.app.color\" :options=\"ColorOptions\" />\n      </div>\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"flex items-center text-16 font-bold\">\n        <div class=\"mr-4\">{{ $t('settings.lang.name') }}</div>\n        <Button type=\"text\" icon=\"link\" @click=\"BrowserOpenURL(APP_LOCALES_URL)\" />\n        <Button type=\"text\" icon=\"folder\" @click=\"handleOpenLocalesFolder\" />\n        <Button\n          v-tips=\"'settings.lang.load'\"\n          :loading=\"appStore.localesLoading\"\n          type=\"text\"\n          icon=\"refresh\"\n          @click=\"appStore.loadLocales()\"\n        />\n      </div>\n      <Radio v-model=\"appSettings.app.lang\" :options=\"appStore.locales\" />\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.fontFamily') }}</div>\n      <Input v-model=\"appSettings.app.fontFamily\" editable class=\"text-14\">\n        <template #suffix>\n          <Button\n            v-tips=\"'settings.resetFont'\"\n            type=\"text\"\n            size=\"small\"\n            icon=\"reset\"\n            @click=\"resetFontFamily\"\n          />\n        </template>\n      </Input>\n    </div>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.pages.name') }}</div>\n      <CheckBox v-model=\"appSettings.app.pages\" :options=\"pages\" />\n    </div>\n  </Card>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/components/SwitchBranch.vue",
    "content": "<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n'\n\nimport { Branch } from '@/enums/app'\nimport { useAppSettingsStore, useKernelApiStore } from '@/stores'\nimport { message } from '@/utils'\n\nconst { t } = useI18n()\nconst appSettings = useAppSettingsStore()\nconst kernelApiStore = useKernelApiStore()\n\nconst handleUseBranch = async (branch: Branch) => {\n  appSettings.app.kernel.branch = branch\n\n  if (!kernelApiStore.running) return\n\n  try {\n    await kernelApiStore.restartCore()\n    message.success('common.success')\n  } catch (error: any) {\n    message.error(error)\n  }\n}\n</script>\n\n<template>\n  <div class=\"font-bold text-16 mx-4 my-12\">{{ t('settings.kernel.version') }}</div>\n  <div class=\"flex gap-8\">\n    <Card\n      :selected=\"appSettings.app.kernel.branch === Branch.Main\"\n      title=\"Stable\"\n      class=\"w-[36%]\"\n      @click=\"handleUseBranch(Branch.Main)\"\n    >\n      <div class=\"py-4 text-12\">\n        {{ t('settings.kernel.stable') }}\n      </div>\n    </Card>\n    <Card\n      :selected=\"appSettings.app.kernel.branch === Branch.Alpha\"\n      title=\"Alpha\"\n      class=\"w-[36%]\"\n      @click=\"handleUseBranch(Branch.Alpha)\"\n    >\n      <div class=\"py-4 text-12\">\n        {{ t('settings.kernel.alpha') }}\n      </div>\n    </Card>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/components/components/SystemProxySettings.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useAppSettingsStore } from '@/stores'\n\nconst appSettings = useAppSettingsStore()\n</script>\n\n<template>\n  <div class=\"px-8 py-12 text-18 font-bold\">{{ $t('settings.systemProxy') }}</div>\n\n  <Card>\n    <div class=\"px-8 py-12 flex items-center justify-between\">\n      <div class=\"text-16 font-bold\">{{ $t('settings.autoSetSystemProxy') }}</div>\n      <Switch v-model=\"appSettings.app.autoSetSystemProxy\" />\n    </div>\n    <div class=\"px-8 pt-12 pb-8 flex flex-col gap-12\">\n      <div class=\"text-16 font-bold\">\n        {{ $t('settings.proxyBypassList') }}\n        <span class=\"font-normal text-12\">({{ $t('settings.proxyBypassListTips') }})</span>\n      </div>\n      <CodeViewer\n        v-model=\"appSettings.app.proxyBypassList\"\n        editable\n        lang=\"yaml\"\n        class=\"min-w-256\"\n      />\n    </div>\n  </Card>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SettingsView/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { defineAsyncComponent, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useAppStore } from '@/stores'\n\nconst settings = [\n  {\n    key: 'general',\n    tab: 'settings.general',\n    component: defineAsyncComponent(() => import('./components/GeneralSettings.vue')),\n  },\n  {\n    key: 'kernel',\n    tab: 'router.kernel',\n    component: defineAsyncComponent(() => import('./components/CoreSettings.vue')),\n  },\n  {\n    key: 'plugins',\n    tab: 'router.plugins',\n    component: defineAsyncComponent(() => import('./components/PluginSettings.vue')),\n  },\n] as const\n\nconst activeKey = ref(settings[0].key)\n\nconst { t } = useI18n()\nconst appStore = useAppStore()\n</script>\n\n<template>\n  <Tabs\n    v-model:active-key=\"activeKey\"\n    :items=\"settings\"\n    tab-width=\"15%\"\n    content-width=\"85%\"\n    class=\"h-full\"\n  >\n    <template #extra>\n      <Button type=\"text\" @click=\"appStore.showAbout = true\">\n        {{ t('router.about') }}\n      </Button>\n    </template>\n  </Tabs>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SubscribesView/components/ProxiesEditor.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, h } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { ReadFile, WriteFile } from '@/bridge'\nimport { useSubscribesStore } from '@/stores'\nimport { deepClone, ignoredError, message, omitArray, sampleID } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\nimport type { Subscription } from '@/types/app'\n\ninterface Props {\n  sub: Subscription\n}\n\nconst props = defineProps<Props>()\n\nconst loading = ref(false)\nconst proxiesText = ref('')\nconst sub = ref(deepClone(props.sub))\n\nconst { t } = useI18n()\nconst subscribeStore = useSubscribesStore()\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst handleSave = async () => {\n  loading.value = true\n  try {\n    const { path, proxies, id } = sub.value\n    const proxiesWithId: Record<string, any>[] = JSON.parse(proxiesText.value)\n    sub.value.proxies = proxiesWithId.map((v) => ({\n      id: proxies.find((proxy) => proxy.id === v.__id_in_gui)?.id || sampleID(),\n      tag: v.tag,\n      type: v.type,\n    }))\n    await WriteFile(path, JSON.stringify(omitArray(proxiesWithId, ['__id_in_gui']), null, 2))\n    await subscribeStore.editSubscribe(id, sub.value)\n    handleSubmit()\n  } catch (error: any) {\n    console.log(error)\n\n    message.error(error.message || error)\n  }\n  loading.value = false\n}\n\nconst initProxiesText = async () => {\n  const content = (await ignoredError(ReadFile, sub.value.path)) || '[]'\n  const proxies: Subscription['proxies'] = JSON.parse(content)\n  const proxiesWithId = proxies.map((proxy) => {\n    return {\n      __id_in_gui: sub.value.proxies.find((v) => v.tag === proxy.tag)?.id || sampleID(),\n      ...proxy,\n    }\n  })\n  proxiesText.value = JSON.stringify(proxiesWithId, null, 2)\n}\n\ninitProxiesText()\n\nconst modalSlots = {\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <CodeViewer v-model=\"proxiesText\" lang=\"json\" editable class=\"h-full\" />\n</template>\n"
  },
  {
    "path": "frontend/src/views/SubscribesView/components/ProxiesView.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, computed, inject, h } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { ClipboardSetText, ReadFile, WriteFile } from '@/bridge'\nimport { DraggableOptions } from '@/constant/app'\nimport { useBool } from '@/hooks'\nimport { useSubscribesStore } from '@/stores'\nimport { buildSmartRegExp, deepClone, ignoredError, message, sampleID } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\nimport type { Menu, Subscription } from '@/types/app'\n\ninterface Props {\n  sub: Subscription\n}\n\nconst props = defineProps<Props>()\n\nlet editId = ''\nconst isEdit = ref(false)\nconst loading = ref(false)\nconst keywords = ref('')\nconst proxyType = ref('')\nconst details = ref()\nconst allFieldsProxies = ref<any[]>([])\nconst sub = ref(deepClone(props.sub))\n\nconst [showDetails, toggleDetails] = useBool(false)\n\nconst filteredProxyTypeOptions = computed(() => {\n  const proxyProtocols = sub.value.proxies.reduce((p, c) => {\n    p[c.type] = (p[c.type] || 0) + 1\n    return p\n  }, {} as Recordable)\n  return [{ label: 'All', value: '', count: 0 }].concat(\n    Object.entries(proxyProtocols).map(([label, count]) => ({\n      label: `${label}(${count})`,\n      value: label,\n      count,\n    })),\n  )\n})\n\nconst filteredProxies = computed(() => {\n  return sub.value.proxies.filter((v) => {\n    const hitType = proxyType.value ? proxyType.value === v.type : true\n    const hitName = buildSmartRegExp(keywords.value, 'i').test(v.tag)\n    return hitName && hitType\n  })\n})\n\nconst menus: Menu[] = [\n  {\n    label: 'common.details',\n    handler: async (record: Subscription['proxies'][0]) => {\n      try {\n        const proxy = await getProxyByTag(record.tag)\n        details.value = JSON.stringify(proxy, null, 2)\n        isEdit.value = false\n        toggleDetails()\n      } catch (error: any) {\n        message.error(error)\n      }\n    },\n  },\n  {\n    label: 'common.copy',\n    handler: async (record: Subscription['proxies'][0]) => {\n      try {\n        const proxy = await getProxyByTag(record.tag)\n        await ClipboardSetText(JSON.stringify(proxy, null, 2))\n        message.success('common.copied')\n      } catch (error: any) {\n        message.error(error)\n      }\n    },\n  },\n  {\n    label: 'common.edit',\n    handler: async (record: Subscription['proxies'][0]) => {\n      try {\n        const proxy = await getProxyByTag(record.tag)\n        details.value = JSON.stringify(proxy, null, 2)\n        isEdit.value = true\n        editId = record.tag\n        toggleDetails()\n      } catch (error: any) {\n        message.error(error)\n      }\n    },\n  },\n  {\n    label: 'common.delete',\n    handler: (record: Record<string, any>) => {\n      const idx = sub.value.proxies.findIndex((v) => v.tag === record.tag)\n      if (idx !== -1) {\n        sub.value.proxies.splice(idx, 1)\n      }\n    },\n  },\n]\n\nconst { t } = useI18n()\nconst subscribeStore = useSubscribesStore()\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst handleSave = async () => {\n  loading.value = true\n  try {\n    const { path, proxies, id } = sub.value\n    await initAllFieldsProxies()\n    const filteredProxies = allFieldsProxies.value.filter((v: any) =>\n      proxies.some((vv) => vv.tag === v.tag),\n    )\n    const sortedArray = proxies.map((v) => filteredProxies.find((vv) => vv.tag === v.tag))\n    await WriteFile(path, JSON.stringify(sortedArray, null, 2))\n    await subscribeStore.editSubscribe(id, sub.value)\n    handleSubmit()\n  } catch (error: any) {\n    console.log(error)\n    message.error(error)\n  }\n  loading.value = false\n}\n\nconst handleAdd = async () => {\n  editId = ''\n  details.value = ''\n  isEdit.value = true\n  toggleDetails()\n}\n\nconst onEditEnd = async () => {\n  let proxy: any\n  try {\n    proxy = JSON.parse(details.value)\n\n    if (typeof proxy !== 'object') throw 'wrong format'\n  } catch (error: any) {\n    console.log(error)\n    message.error(error.message || error)\n    // reopen\n    toggleDetails()\n    return\n  }\n\n  await initAllFieldsProxies()\n\n  const allFieldsProxiesIdx = allFieldsProxies.value.findIndex((v: any) => v.tag === editId)\n  const subProxiesIdx = sub.value.proxies.findIndex((v) => v.tag === editId)\n\n  if (allFieldsProxiesIdx !== -1 && subProxiesIdx !== -1) {\n    allFieldsProxies.value.splice(allFieldsProxiesIdx, 1, proxy)\n    sub.value.proxies.splice(subProxiesIdx, 1, {\n      ...sub.value.proxies[subProxiesIdx]!,\n      tag: proxy.tag,\n    })\n  } else {\n    allFieldsProxies.value.push(proxy)\n    sub.value.proxies.push({\n      id: sampleID(),\n      tag: proxy.tag,\n      type: proxy.type,\n    })\n  }\n}\n\nconst initAllFieldsProxies = async () => {\n  if (allFieldsProxies.value.length) return\n  const content = (await ignoredError(ReadFile, sub.value!.path)) || '[]'\n  allFieldsProxies.value = JSON.parse(content)\n}\n\nconst getProxyByTag = async (tag: string) => {\n  await initAllFieldsProxies()\n  const proxy = allFieldsProxies.value.find((v: any) => v.tag === tag)\n  if (!proxy) throw 'Proxy Not Found'\n  return proxy\n}\n\nconst modalSlots = {\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div class=\"h-full flex flex-col\">\n    <div class=\"flex items-center\">\n      <span class=\"mr-8\">\n        {{ t('subscribes.proxies.type') }}\n        :\n      </span>\n      <Select v-model=\"proxyType\" :options=\"filteredProxyTypeOptions\" size=\"small\" />\n      <Input\n        v-model=\"keywords\"\n        :placeholder=\"t('subscribes.proxies.name')\"\n        clearable\n        size=\"small\"\n        class=\"mx-8 flex-1\"\n      />\n      <Button type=\"primary\" size=\"small\" @click=\"handleAdd\">\n        {{ t('subscribes.proxies.add') }}\n      </Button>\n    </div>\n\n    <Empty v-if=\"filteredProxies.length === 0\" />\n\n    <div\n      v-else\n      v-draggable=\"[sub.proxies, DraggableOptions]\"\n      class=\"grid grid-cols-4 gap-8 mt-8 overflow-y-auto\"\n    >\n      <Card\n        v-for=\"proxy in filteredProxies\"\n        :key=\"proxy.tag\"\n        v-menu=\"menus.map((v) => ({ ...v, handler: () => v.handler?.(proxy) }))\"\n        :title=\"proxy.tag\"\n      >\n        <div class=\"text-12\">\n          {{ proxy.type }}\n        </div>\n      </Card>\n    </div>\n  </div>\n\n  <Modal\n    v-model:open=\"showDetails\"\n    :submit=\"isEdit\"\n    :mask-closable=\"!isEdit\"\n    :title=\"isEdit ? (details ? 'common.edit' : 'common.add') : 'common.details'\"\n    :on-ok=\"onEditEnd\"\n    cancel-text=\"common.close\"\n    max-height=\"80\"\n    max-width=\"80\"\n  >\n    <CodeViewer v-model=\"details\" lang=\"json\" :editable=\"isEdit\" />\n  </Modal>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SubscribesView/components/SubscribeForm.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, computed, h } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { RequestMethodOptions } from '@/constant/app'\nimport { useBool } from '@/hooks'\nimport { useSubscribesStore } from '@/stores'\nimport { deepClone, message } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\nimport type { Subscription } from '@/types/app'\n\ninterface Props {\n  id?: string\n}\n\nconst props = defineProps<Props>()\n\nconst { t } = useI18n()\nconst [showMore, toggleShowMore] = useBool(false)\nconst subscribeStore = useSubscribesStore()\n\nconst loading = ref(false)\nconst sub = ref<Subscription>(subscribeStore.getSubscribeTemplate())\n\nconst isManual = computed(() => sub.value.type === 'Manual')\nconst isRemote = computed(() => sub.value.type === 'Http')\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst handleSave = async () => {\n  loading.value = true\n\n  try {\n    if (props.id) {\n      await subscribeStore.editSubscribe(props.id, sub.value)\n    } else {\n      await subscribeStore.addSubscribe(sub.value)\n    }\n    await handleSubmit()\n  } catch (error: any) {\n    console.error(error)\n    message.error(error)\n  }\n\n  loading.value = false\n}\n\nif (props.id) {\n  const s = subscribeStore.getSubscribeById(props.id)\n  if (s) {\n    sub.value = deepClone(s)\n  }\n}\n\nconst modalSlots = {\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        disabled: !sub.value.name || !sub.value.path || (!sub.value.url && !isManual.value),\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div>\n    <div class=\"form-item\">\n      {{ t('subscribes.subtype') }}\n      <Radio\n        v-model=\"sub.type\"\n        :options=\"[\n          { label: 'common.http', value: 'Http' },\n          { label: 'common.file', value: 'File' },\n          { label: 'subscribe.manual', value: 'Manual' },\n        ]\"\n      />\n    </div>\n    <div class=\"form-item\">\n      {{ t('subscribe.name') }} *\n      <div class=\"min-w-[75%]\">\n        <Input v-model=\"sub.name\" autofocus class=\"w-full\" />\n      </div>\n    </div>\n    <div v-if=\"!isManual\" class=\"form-item\">\n      {{ t(sub.type === 'Http' ? 'subscribe.url' : 'subscribe.localPath') }} *\n      <div class=\"min-w-[75%]\">\n        <Input\n          v-model=\"sub.url\"\n          :placeholder=\"sub.type === 'Http' ? 'http(s)://' : 'data/local/{filename}.json'\"\n          allow-paste\n          class=\"w-full\"\n        />\n      </div>\n    </div>\n    <div class=\"form-item\">\n      {{ t('subscribe.path') }} *\n      <div class=\"min-w-[75%]\">\n        <Input v-model=\"sub.path\" placeholder=\"data/subscribes/{filename}.json\" class=\"w-full\" />\n      </div>\n    </div>\n    <Divider v-if=\"!isManual\">\n      <Button type=\"text\" size=\"small\" @click=\"toggleShowMore\">\n        {{ t('common.more') }}\n      </Button>\n    </Divider>\n    <div v-if=\"showMore && !isManual\">\n      <div class=\"form-item\">\n        {{ t('subscribe.include') }}\n        <div class=\"min-w-[75%]\">\n          <Input v-model=\"sub.include\" placeholder=\"keyword1|keyword2\" class=\"w-full\" />\n        </div>\n      </div>\n      <div class=\"form-item\">\n        {{ t('subscribe.exclude') }}\n        <div class=\"min-w-[75%]\">\n          <Input v-model=\"sub.exclude\" placeholder=\"keyword1|keyword2\" class=\"w-full\" />\n        </div>\n      </div>\n      <div class=\"form-item\">\n        {{ t('subscribe.includeProtocol') }}\n        <div class=\"min-w-[75%]\">\n          <Input\n            v-model=\"sub.includeProtocol\"\n            placeholder=\"direct|mixed|socks|http...\"\n            class=\"w-full\"\n          />\n        </div>\n      </div>\n      <div class=\"form-item\">\n        {{ t('subscribe.excludeProtocol') }}\n        <div class=\"min-w-[75%]\">\n          <Input\n            v-model=\"sub.excludeProtocol\"\n            placeholder=\"direct|mixed|socks|http...\"\n            class=\"w-full\"\n          />\n        </div>\n      </div>\n      <div class=\"form-item\">\n        {{ t('subscribe.proxyPrefix') }}\n        <div class=\"min-w-[75%]\">\n          <Input v-model=\"sub.proxyPrefix\" class=\"w-full\" />\n        </div>\n      </div>\n      <template v-if=\"isRemote\">\n        <div class=\"form-item\">\n          {{ t('subscribe.website') }}\n          <div class=\"min-w-[75%]\">\n            <Input v-model=\"sub.website\" placeholder=\"http(s)://\" class=\"w-full\" />\n          </div>\n        </div>\n        <div class=\"form-item\">\n          {{ t('subscribe.inSecure') }}\n          <Switch v-model=\"sub.inSecure\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('subscribe.requestTimeout') }}\n          <Input v-model=\"sub.requestTimeout\" type=\"number\" :min=\"3\" :max=\"180\" />\n        </div>\n        <div class=\"form-item\">\n          {{ t('subscribe.requestMethod') }}\n          <Radio v-model=\"sub.requestMethod\" :options=\"RequestMethodOptions\" />\n        </div>\n        <div\n          :class=\"{ 'items-start': Object.keys(sub.header.request).length !== 0 }\"\n          class=\"form-item\"\n        >\n          {{ t('subscribe.header.request') }}\n          <KeyValueEditor v-model=\"sub.header.request\" />\n        </div>\n        <div\n          :class=\"{ 'items-start': Object.keys(sub.header.response).length !== 0 }\"\n          class=\"form-item\"\n        >\n          {{ t('subscribe.header.response') }}\n          <KeyValueEditor v-model=\"sub.header.response\" />\n        </div>\n      </template>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SubscribesView/components/SubscribeScript.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, inject, h } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useSubscribesStore } from '@/stores'\nimport { message } from '@/utils'\n\nimport Button from '@/components/Button/index.vue'\n\nimport type { Subscription } from '@/types/app'\n\ninterface Props {\n  id: string\n}\n\nconst props = defineProps<Props>()\n\nconst loading = ref(false)\nconst subscribe = ref<Subscription>()\nconst code = ref('')\n\nconst { t } = useI18n()\nconst subscribeStore = useSubscribesStore()\n\nconst handleCancel = inject('cancel') as any\nconst handleSubmit = inject('submit') as any\n\nconst handleSave = async () => {\n  if (!subscribe.value) return\n  loading.value = true\n  try {\n    subscribe.value.script = code.value\n    await subscribeStore.editSubscribe(props.id, subscribe.value)\n    handleSubmit()\n  } catch (error: any) {\n    message.error(error)\n    console.log(error)\n  }\n  loading.value = false\n}\n\nconst s = subscribeStore.getSubscribeById(props.id)\nif (s) {\n  subscribe.value = s\n  code.value = s.script\n}\n\nconst modalSlots = {\n  cancel: () =>\n    h(\n      Button,\n      {\n        disabled: loading.value,\n        onClick: handleCancel,\n      },\n      () => t('common.cancel'),\n    ),\n  submit: () =>\n    h(\n      Button,\n      {\n        type: 'primary',\n        loading: loading.value,\n        onClick: handleSave,\n      },\n      () => t('common.save'),\n    ),\n}\n\ndefineExpose({ modalSlots })\n</script>\n\n<template>\n  <div>\n    <CodeViewer v-model=\"code\" lang=\"javascript\" editable />\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/SubscribesView/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent } from 'vue'\nimport { useI18n, I18nT } from 'vue-i18n'\n\nimport { BrowserOpenURL, ClipboardSetText, RemoveFile } from '@/bridge'\nimport { DraggableOptions, ViewOptions } from '@/constant/app'\nimport { View } from '@/enums/app'\nimport { useSubscribesStore, useAppSettingsStore, usePluginsStore, useAppStore } from '@/stores'\nimport {\n  formatBytes,\n  formatRelativeTime,\n  debounce,\n  ignoredError,\n  formatDate,\n  message,\n} from '@/utils'\n\nimport { useModal } from '@/components/Modal'\n\nimport type { Menu, Subscription } from '@/types/app'\n\nconst ProxiesEditor = defineAsyncComponent(() => import('./components/ProxiesEditor.vue'))\nconst ProxiesView = defineAsyncComponent(() => import('./components/ProxiesView.vue'))\nconst SubscribeForm = defineAsyncComponent(() => import('./components/SubscribeForm.vue'))\nconst SubscribeScript = defineAsyncComponent(() => import('./components/SubscribeScript.vue'))\n\nconst menuList: Menu[] = [\n  {\n    label: 'subscribes.editProxies',\n    handler: (id: string) => handleEditProxies(id),\n  },\n  {\n    label: 'subscribes.editSourceFile',\n    handler: (id: string) => handleEditProxies(id, true),\n  },\n  {\n    label: 'subscribes.copySub',\n    handler: async (id: string) => {\n      const sub = subscribeStore.getSubscribeById(id)!\n      if (sub) {\n        await ClipboardSetText(sub.url)\n        message.success('common.copied')\n      }\n    },\n  },\n  {\n    label: 'subscribes.script',\n    handler: async (id: string) => {\n      modalApi.setProps({ title: 'common.edit', width: '90' })\n      modalApi.setContent(SubscribeScript, { id }).open()\n    },\n  },\n]\n\nconst { t } = useI18n()\nconst [Modal, modalApi] = useModal({})\nconst appStore = useAppStore()\nconst subscribeStore = useSubscribesStore()\nconst appSettingsStore = useAppSettingsStore()\nconst pluginsStore = usePluginsStore()\n\nconst generateMenus = (subscription: Subscription) => {\n  const builtInMenus: Menu[] = menuList.map((v) => ({\n    ...v,\n    handler: () => v.handler?.(subscription.id),\n  }))\n\n  const contextMenus = pluginsStore.plugins.filter(\n    (plugin) => Object.keys(plugin.context.subscriptions).length !== 0,\n  )\n\n  if (contextMenus.length !== 0) {\n    builtInMenus.push(\n      {\n        label: '',\n        separator: true,\n      },\n      {\n        label: 'common.more',\n        children: contextMenus.reduce((prev, plugin) => {\n          const menus = Object.entries(plugin.context.subscriptions)\n          return prev.concat(\n            menus.map(([title, fn]) => {\n              return {\n                label: title,\n                handler: async () => {\n                  try {\n                    plugin.running = true\n                    await pluginsStore.manualTrigger(plugin.id, fn as any, subscription)\n                  } catch (error: any) {\n                    message.error(error)\n                  } finally {\n                    plugin.running = false\n                  }\n                },\n              }\n            }),\n          )\n        }, [] as Menu[]),\n      },\n    )\n  }\n\n  return builtInMenus\n}\n\nconst handleShowSubForm = (id?: string) => {\n  modalApi.setProps({\n    title: id ? 'common.edit' : 'common.add',\n    minWidth: '70',\n  })\n  modalApi.setContent(SubscribeForm, { id }).open()\n}\n\nconst handleUpdateSubs = async () => {\n  try {\n    await subscribeStore.updateSubscribes()\n    message.success('common.success')\n  } catch (error: any) {\n    console.error('updateSubscribes: ', error)\n    message.error(error)\n  }\n}\n\nconst handleEditProxies = (id: string, editor = false) => {\n  const sub = subscribeStore.getSubscribeById(id)\n  if (sub) {\n    modalApi.setProps({ title: sub.name, height: '90', width: '90' })\n    modalApi.setContent(editor ? ProxiesEditor : ProxiesView, { sub }).open()\n  }\n}\n\nconst handleUpdateSub = async (s: Subscription) => {\n  try {\n    await subscribeStore.updateSubscribe(s.id)\n  } catch (error: any) {\n    console.error('updateSubscribe: ', error)\n    message.error(error)\n  }\n}\n\nconst handleDeleteSub = async (s: Subscription) => {\n  try {\n    await ignoredError(RemoveFile, s.path)\n    await subscribeStore.deleteSubscribe(s.id)\n  } catch (error: any) {\n    console.error('deleteSubscribe: ', error)\n    message.error(error)\n  }\n}\n\nconst handleDisableSub = async (s: Subscription) => {\n  s.disabled = !s.disabled\n  subscribeStore.editSubscribe(s.id, s)\n}\n\nconst noUpdateNeeded = computed(() => subscribeStore.subscribes.every((v) => v.disabled))\n\nconst clacTrafficPercent = (s: Subscription) => ((s.upload + s.download) / s.total) * 100\n\nconst clacTrafficStatus = (s: Subscription) => {\n  const percent = clacTrafficPercent(s)\n  if (percent > 90) return 'danger'\n  if (percent > 80) return 'warning'\n  return 'primary'\n}\n\nconst onSortUpdate = debounce(subscribeStore.saveSubscribes, 1000)\n</script>\n\n<template>\n  <div v-if=\"subscribeStore.subscribes.length === 0\" class=\"grid-list-empty\">\n    <Empty>\n      <template #description>\n        <I18nT keypath=\"subscribes.empty\" tag=\"div\" scope=\"global\" class=\"flex items-center mt-12\">\n          <template #action>\n            <Button type=\"link\" @click=\"handleShowSubForm()\">{{ t('common.add') }}</Button>\n          </template>\n        </I18nT>\n        <div class=\"flex items-center\">\n          <CustomAction :actions=\"appStore.customActions.subscriptions_header\" />\n        </div>\n      </template>\n    </Empty>\n  </div>\n\n  <div v-else class=\"grid-list-header\">\n    <Radio v-model=\"appSettingsStore.app.subscribesView\" :options=\"ViewOptions\" class=\"mr-auto\" />\n    <CustomAction :actions=\"appStore.customActions.subscriptions_header\" />\n    <Button\n      :disabled=\"noUpdateNeeded\"\n      :type=\"noUpdateNeeded ? 'text' : 'link'\"\n      @click=\"handleUpdateSubs\"\n    >\n      {{ t('common.updateAll') }}\n    </Button>\n    <Button type=\"primary\" icon=\"add\" class=\"ml-16\" @click=\"handleShowSubForm()\">\n      {{ t('common.add') }}\n    </Button>\n  </div>\n\n  <div\n    v-draggable=\"[subscribeStore.subscribes, { ...DraggableOptions, onUpdate: onSortUpdate }]\"\n    :class=\"'grid-list-' + appSettingsStore.app.subscribesView\"\n  >\n    <Card\n      v-for=\"s in subscribeStore.subscribes\"\n      :key=\"s.id\"\n      v-menu=\"generateMenus(s)\"\n      :title=\"s.name\"\n      :disabled=\"s.disabled\"\n      class=\"grid-list-item\"\n    >\n      <template #title-prefix>\n        <Tag v-if=\"s.updating\" color=\"cyan\" size=\"small\">\n          {{ t('subscribe.updating') }}\n        </Tag>\n      </template>\n\n      <template #title-suffix>\n        <Icon\n          v-if=\"s.type !== 'File' && s.website\"\n          v-tips=\"'subscribe.website'\"\n          icon=\"link\"\n          color=\"var(--card-color)\"\n          class=\"mx-4 cursor-pointer shrink-0\"\n          @click=\"BrowserOpenURL(s.website)\"\n        />\n      </template>\n\n      <template v-if=\"appSettingsStore.app.subscribesView === View.Grid\" #extra>\n        <Dropdown>\n          <Button type=\"link\" size=\"small\" icon=\"more\" />\n          <template #overlay>\n            <div class=\"flex flex-col gap-4 min-w-64 p-4\">\n              <Button\n                :disabled=\"s.disabled\"\n                :loading=\"s.updating\"\n                :type=\"s.disabled ? 'text' : 'text'\"\n                @click=\"handleUpdateSub(s)\"\n              >\n                {{ t('common.update') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleDisableSub(s)\">\n                {{ s.disabled ? t('common.enable') : t('common.disable') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleShowSubForm(s.id)\">\n                {{ t('common.edit') }}\n              </Button>\n              <Button type=\"text\" @click=\"handleDeleteSub(s)\">\n                {{ t('common.delete') }}\n              </Button>\n            </div>\n          </template>\n        </Dropdown>\n      </template>\n\n      <template v-else #extra>\n        <Button\n          :disabled=\"s.disabled\"\n          :loading=\"s.updating\"\n          :type=\"s.disabled ? 'text' : 'text'\"\n          size=\"small\"\n          @click=\"handleUpdateSub(s)\"\n        >\n          {{ t('common.update') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" @click=\"handleDisableSub(s)\">\n          {{ s.disabled ? t('common.enable') : t('common.disable') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" @click=\"handleShowSubForm(s.id)\">\n          {{ t('common.edit') }}\n        </Button>\n        <Button type=\"text\" size=\"small\" @click=\"handleDeleteSub(s)\">\n          {{ t('common.delete') }}\n        </Button>\n      </template>\n      <template v-if=\"appSettingsStore.app.subscribesView === View.List\">\n        <div style=\"margin-bottom: 8px\">\n          <Progress :percent=\"clacTrafficPercent(s)\" :status=\"clacTrafficStatus(s)\" />\n        </div>\n        <div>\n          {{ t('subscribes.proxyCount') }}\n          :\n          {{ s.proxies.length }}\n        </div>\n        <div>\n          {{ t('subscribes.upload') }}\n          :\n          {{ s.upload ? formatBytes(s.upload, 2) : '--' }}\n          /\n          {{ t('subscribes.download') }}\n          :\n          {{ s.download ? formatBytes(s.download, 2) : '--' }}\n          /\n          {{ t('subscribes.total') }}\n          :\n          {{ s.total ? formatBytes(s.total, 2) : '--' }}\n        </div>\n        <div>\n          {{ t('subscribes.expire') }}\n          :\n          {{ s.expire ? formatDate(s.expire, 'YYYY-MM-DD HH:mm:ss') : '--' }}\n          /\n          {{ t('common.updateTime') }}\n          :\n          {{ s.updateTime ? formatDate(s.updateTime, 'YYYY-MM-DD HH:mm:ss') : '--' }}\n        </div>\n      </template>\n      <template v-else>\n        <div>\n          {{ s.upload + s.download ? formatBytes(s.upload + s.download) : '--' }}\n          /\n          {{ s.total ? formatBytes(s.total) : '--' }}\n        </div>\n        <div class=\"traffic-diagram\">\n          <Progress\n            :percent=\"clacTrafficPercent(s)\"\n            :status=\"clacTrafficStatus(s)\"\n            type=\"circle\"\n            :radius=\"20\"\n          />\n        </div>\n        <div>\n          {{ t('subscribes.expire') }}\n          :\n          {{ s.expire ? formatRelativeTime(s.expire) : '--' }}\n        </div>\n        <div>\n          {{ t('common.updateTime') }}\n          :\n          {{ s.updateTime ? formatRelativeTime(s.updateTime) : '--' }}\n        </div>\n      </template>\n    </Card>\n  </div>\n\n  <Modal />\n</template>\n\n<style lang=\"less\" scoped>\n.traffic-diagram {\n  position: absolute;\n  top: 40px;\n  right: 12px;\n}\n</style>\n"
  },
  {
    "path": "frontend/tsconfig.app.json",
    "content": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"include\": [\"env.d.ts\", \"src/**/*\", \"src/**/*.vue\"],\n  \"exclude\": [\"src/**/__tests__/*\"],\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"baseUrl\": \".\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n      \"@wails/*\": [\"./src/bridge/wailsjs/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    },\n    {\n      \"path\": \"./tsconfig.app.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "frontend/tsconfig.node.json",
    "content": "{\n  \"extends\": \"@tsconfig/node24/tsconfig.json\",\n  \"include\": [\"vite.config.*\", \"eslint.config.*\"],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"noEmit\": true,\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "frontend/vite.config.ts",
    "content": "import { fileURLToPath, URL } from 'node:url'\n\nimport vue from '@vitejs/plugin-vue'\nimport { defineConfig } from 'vite'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  base: './',\n  plugins: [vue()],\n  resolve: {\n    extensions: ['.ts', '.js'],\n    alias: {\n      '@': fileURLToPath(new URL('./src', import.meta.url)),\n      '@wails': fileURLToPath(new URL('./src/bridge/wailsjs', import.meta.url)),\n      vue: 'vue/dist/vue.esm-bundler.js',\n    },\n  },\n  build: {\n    cssCodeSplit: false,\n    chunkSizeWarningLimit: 4096, // 4MB\n    rolldownOptions: {\n      output: {\n        codeSplitting: {\n          groups: [\n            { name: 'vue', test: /node_modules\\/vue/ },\n            { name: 'codemirror', test: /node_modules\\/@codemirror/ },\n            { name: 'prettier', test: /node_modules\\/prettier/ },\n            { name: 'vendor', test: /node_modules/ },\n            { name: 'index' },\n          ],\n        },\n      },\n    },\n  },\n})\n"
  },
  {
    "path": "go.mod",
    "content": "module guiforcores\n\ngo 1.26\n\nrequire (\n\tgithub.com/energye/systray v1.0.3\n\tgithub.com/gen2brain/beeep v0.11.2\n\tgithub.com/oschwald/geoip2-golang v1.13.0\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c\n\tgithub.com/shirou/gopsutil/v3 v3.24.5\n\tgithub.com/wailsapp/wails/v2 v2.11.0\n\tgolang.org/x/sys v0.41.0\n\tgolang.org/x/text v0.34.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgit.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect\n\tgithub.com/bep/debounce v1.2.1 // indirect\n\tgithub.com/esiqveland/notify v0.13.3 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/godbus/dbus/v5 v5.2.2 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/gorilla/websocket v1.5.3 // indirect\n\tgithub.com/jackmordaunt/icns/v3 v3.0.1 // indirect\n\tgithub.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/labstack/echo/v4 v4.15.1 // indirect\n\tgithub.com/labstack/gommon v0.4.2 // indirect\n\tgithub.com/leaanthony/go-ansi-parser v1.6.1 // indirect\n\tgithub.com/leaanthony/gosod v1.0.4 // indirect\n\tgithub.com/leaanthony/slicer v1.6.0 // indirect\n\tgithub.com/leaanthony/u v1.1.1 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect\n\tgithub.com/oschwald/maxminddb-golang v1.13.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/samber/lo v1.52.0 // indirect\n\tgithub.com/sergeymakinen/go-bmp v1.0.0 // indirect\n\tgithub.com/sergeymakinen/go-ico v1.0.0 // indirect\n\tgithub.com/shoenig/go-m1cpu v0.1.7 // indirect\n\tgithub.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/tkrajina/go-reflector v0.5.8 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasttemplate v1.2.2 // indirect\n\tgithub.com/wailsapp/go-webview2 v1.0.23 // indirect\n\tgithub.com/wailsapp/mimetype v1.4.1 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/net v0.51.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n)\n\nreplace github.com/energye/systray => github.com/GUI-for-Cores/systray v1.0.1\n"
  },
  {
    "path": "go.sum",
    "content": "git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE=\ngit.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=\ngithub.com/GUI-for-Cores/systray v1.0.1 h1:nJEIsm3yHoYhQD7PnUxCD2bOP/+axLt/bWLWHB8OWT8=\ngithub.com/GUI-for-Cores/systray v1.0.1/go.mod h1:HelKhC3PXwv3ryDxbuQqV+7kAxAYNzE5cfdrerGOZTc=\ngithub.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=\ngithub.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o=\ngithub.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE=\ngithub.com/gen2brain/beeep v0.11.2 h1:+KfiKQBbQCuhfJFPANZuJ+oxsSKAYNe88hIpJuyKWDA=\ngithub.com/gen2brain/beeep v0.11.2/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=\ngithub.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=\ngithub.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ=\ngithub.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=\ngithub.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs=\ngithub.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=\ngithub.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=\ngithub.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=\ngithub.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=\ngithub.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=\ngithub.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=\ngithub.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=\ngithub.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=\ngithub.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=\ngithub.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=\ngithub.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=\ngithub.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=\ngithub.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=\ngithub.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=\ngithub.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=\ngithub.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=\ngithub.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=\ngithub.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=\ngithub.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=\ngithub.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=\ngithub.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=\ngithub.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=\ngithub.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=\ngithub.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=\ngithub.com/sergeymakinen/go-ico v1.0.0 h1:uL3khgvKkY6WfAetA+RqsguClBuu7HpvBB/nq/Jvr80=\ngithub.com/sergeymakinen/go-ico v1.0.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=\ngithub.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=\ngithub.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=\ngithub.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0=\ngithub.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=\ngithub.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=\ngithub.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=\ngithub.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=\ngithub.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=\ngithub.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=\ngithub.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=\ngithub.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=\ngithub.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=\ngithub.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=\ngithub.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=\ngithub.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"guiforcores/bridge\"\n\t\"time\"\n\n\t\"github.com/wailsapp/wails/v2\"\n\t\"github.com/wailsapp/wails/v2/pkg/logger\"\n\t\"github.com/wailsapp/wails/v2/pkg/options\"\n\t\"github.com/wailsapp/wails/v2/pkg/options/assetserver\"\n\t\"github.com/wailsapp/wails/v2/pkg/options/linux\"\n\t\"github.com/wailsapp/wails/v2/pkg/options/mac\"\n\t\"github.com/wailsapp/wails/v2/pkg/options/windows\"\n\t\"github.com/wailsapp/wails/v2/pkg/runtime\"\n)\n\n//go:embed all:frontend/dist\nvar assets embed.FS\n\n//go:embed frontend/dist/favicon.ico\nvar icon []byte\n\nfunc main() {\n\tapp := bridge.CreateApp(assets)\n\n\ttrayStart, trayEnd := bridge.CreateTray(app, icon)\n\n\t// Create application with options\n\terr := wails.Run(&options.App{\n\t\tMinWidth:         600,\n\t\tMinHeight:        400,\n\t\tDisableResize:    false,\n\t\tMenu:             app.AppMenu,\n\t\tTitle:            bridge.Env.AppName,\n\t\tFrameless:        bridge.Env.OS != \"darwin\",\n\t\tWidth:            bridge.Config.Width,\n\t\tHeight:           bridge.Config.Height,\n\t\tStartHidden:      bridge.Config.StartHidden,\n\t\tWindowStartState: options.WindowStartState(bridge.Config.WindowStartState),\n\t\tBackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 1},\n\t\tWindows: &windows.Options{\n\t\t\tWebviewIsTransparent: true,\n\t\t\tWindowIsTranslucent:  true,\n\t\t\tBackdropType:         windows.Acrylic,\n\t\t\tWebviewBrowserPath:   bridge.Env.WebviewPath,\n\t\t},\n\t\tMac: &mac.Options{\n\t\t\tTitleBar:             mac.TitleBarHiddenInset(),\n\t\t\tAppearance:           mac.DefaultAppearance,\n\t\t\tWebviewIsTransparent: true,\n\t\t\tWindowIsTranslucent:  true,\n\t\t\tAbout: &mac.AboutInfo{\n\t\t\t\tTitle:   bridge.Env.AppName,\n\t\t\t\tMessage: \"© 2026 GUI.for.Cores\",\n\t\t\t\tIcon:    icon,\n\t\t\t},\n\t\t},\n\t\tLinux: &linux.Options{\n\t\t\tIcon:                icon,\n\t\t\tWindowIsTranslucent: false,\n\t\t\tProgramName:         bridge.Env.AppName,\n\t\t\tWebviewGpuPolicy:    linux.WebviewGpuPolicy(bridge.Config.WebviewGpuPolicy),\n\t\t},\n\t\tAssetServer: &assetserver.Options{\n\t\t\tAssets:     assets,\n\t\t\tMiddleware: bridge.RollingRelease,\n\t\t},\n\t\tSingleInstanceLock: &options.SingleInstanceLock{\n\t\t\tUniqueId: func() string {\n\t\t\t\tif bridge.Config.MultipleInstance {\n\t\t\t\t\treturn time.Now().String()\n\t\t\t\t}\n\t\t\t\treturn bridge.Env.AppName\n\t\t\t}(),\n\t\t\tOnSecondInstanceLaunch: func(data options.SecondInstanceData) {\n\t\t\t\truntime.Show(app.Ctx)\n\t\t\t\truntime.EventsEmit(app.Ctx, \"onLaunchApp\", data.Args)\n\t\t\t},\n\t\t},\n\t\tOnStartup: func(ctx context.Context) {\n\t\t\tapp.Ctx = ctx\n\t\t\ttrayStart()\n\t\t},\n\t\tOnBeforeClose: func(ctx context.Context) (prevent bool) {\n\t\t\tif !bridge.Env.PreventExit {\n\t\t\t\ttrayEnd()\n\t\t\t\treturn false\n\t\t\t}\n\t\t\truntime.EventsEmit(ctx, \"onBeforeExitApp\")\n\t\t\treturn true\n\t\t},\n\t\tBind: []any{\n\t\t\tapp,\n\t\t},\n\t\tLogLevel: logger.INFO,\n\t\tDebug: options.Debug{\n\t\t\tOpenInspectorOnStartup: true,\n\t\t},\n\t})\n\n\tif err != nil {\n\t\tprintln(\"Error:\", err.Error())\n\t}\n}\n"
  },
  {
    "path": "wails.json",
    "content": "{\n  \"$schema\": \"https://wails.io/schemas/config.v2.json\",\n  \"name\": \"GUI.for.SingBox\",\n  \"outputfilename\": \"GUI.for.SingBox\",\n  \"frontend:install\": \"pnpm install\",\n  \"frontend:build\": \"pnpm run build\",\n  \"frontend:dev:watcher\": \"pnpm run dev\",\n  \"frontend:dev:serverUrl\": \"auto\",\n  \"wailsjsdir\": \"frontend/src/bridge\",\n  \"author\": {\n    \"name\": \"GUI.for.Cores\",\n    \"email\": \"GUI.for.Cores@github.com\"\n  },\n  \"info\": {\n    \"copyright\": \"Copyright\",\n    \"comments\": \"https://github.com/GUI-for-Cores\"\n  }\n}\n"
  }
]