├── .editorconfig
├── .eslintignore
├── .eslintrc.cjs
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report_zh.yml
│ ├── config.yml
│ └── feature_request_zh.yml
└── workflows
│ ├── build.yml
│ └── issues.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.yaml
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── LICENSE
├── README.md
├── aur
├── sparkle-bin
│ ├── PKGBUILD
│ ├── sparkle.install
│ └── sparkle.sh
├── sparkle-electron-bin
│ ├── PKGBUILD
│ ├── sparkle.desktop
│ ├── sparkle.install
│ └── sparkle.sh
├── sparkle-electron-git
│ ├── PKGBUILD
│ ├── sparkle.desktop
│ ├── sparkle.install
│ └── sparkle.sh
├── sparkle-electron
│ ├── PKGBUILD
│ ├── sparkle.desktop
│ ├── sparkle.install
│ └── sparkle.sh
├── sparkle-git
│ ├── PKGBUILD
│ ├── sparkle.install
│ └── sparkle.sh
└── sparkle
│ ├── PKGBUILD
│ ├── sparkle.install
│ └── sparkle.sh
├── build
├── entitlements.mac.plist
├── icon.icns
├── icon.ico
├── icon.png
├── installerIcon.ico
├── linux
│ └── postinst
└── pkg-scripts
│ ├── postinstall
│ └── preinstall
├── changelog.md
├── electron-builder.yml
├── electron.vite.config.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── resources
├── icon.ico
├── icon.png
├── iconTemplate.png
└── subStoreIcon.png
├── scripts
├── checksum.mjs
├── prepare.mjs
├── telegram.mjs
└── updater.mjs
├── src
├── main
│ ├── config
│ │ ├── app.ts
│ │ ├── controledMihomo.ts
│ │ ├── index.ts
│ │ ├── override.ts
│ │ └── profile.ts
│ ├── core
│ │ ├── factory.ts
│ │ ├── manager.ts
│ │ ├── mihomoApi.ts
│ │ ├── profileUpdater.ts
│ │ └── subStoreApi.ts
│ ├── index.ts
│ ├── resolve
│ │ ├── autoUpdater.ts
│ │ ├── backup.ts
│ │ ├── floatingWindow.ts
│ │ ├── gistApi.ts
│ │ ├── server.ts
│ │ ├── shortcut.ts
│ │ ├── theme.ts
│ │ ├── trafficMonitor.ts
│ │ └── tray.ts
│ ├── sys
│ │ ├── autoRun.ts
│ │ ├── interface.ts
│ │ ├── misc.ts
│ │ ├── ssid.ts
│ │ └── sysproxy.ts
│ └── utils
│ │ ├── calc.ts
│ │ ├── dirs.ts
│ │ ├── image.ts
│ │ ├── init.ts
│ │ ├── ipc.ts
│ │ ├── merge.ts
│ │ └── template.ts
├── preload
│ ├── index.d.ts
│ └── index.ts
├── renderer
│ ├── floating.html
│ ├── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── FloatingApp.tsx
│ │ ├── assets
│ │ ├── floating.css
│ │ └── main.css
│ │ ├── components
│ │ ├── base
│ │ │ ├── base-confirm.tsx
│ │ │ ├── base-editor.tsx
│ │ │ ├── base-error-boundary.tsx
│ │ │ ├── base-list-editor.tsx
│ │ │ ├── base-page.tsx
│ │ │ ├── base-setting-card.tsx
│ │ │ ├── base-setting-item.tsx
│ │ │ ├── border-switch.css
│ │ │ ├── border-swtich.tsx
│ │ │ ├── collapse-input.tsx
│ │ │ ├── interface-select.tsx
│ │ │ ├── mihomo-icon.tsx
│ │ │ └── substore-icon.tsx
│ │ ├── connections
│ │ │ ├── connection-detail-modal.tsx
│ │ │ └── connection-item.tsx
│ │ ├── logs
│ │ │ └── log-item.tsx
│ │ ├── mihomo
│ │ │ ├── advanced-settings.tsx
│ │ │ ├── controller-setting.tsx
│ │ │ ├── env-setting.tsx
│ │ │ ├── interface-modal.tsx
│ │ │ └── port-setting.tsx
│ │ ├── override
│ │ │ ├── edit-file-modal.tsx
│ │ │ ├── edit-info-modal.tsx
│ │ │ ├── exec-log-modal.tsx
│ │ │ └── override-item.tsx
│ │ ├── profiles
│ │ │ ├── edit-file-modal.tsx
│ │ │ ├── edit-info-modal.tsx
│ │ │ └── profile-item.tsx
│ │ ├── proxies
│ │ │ └── proxy-item.tsx
│ │ ├── resources
│ │ │ ├── geo-data.tsx
│ │ │ ├── proxy-provider.tsx
│ │ │ ├── rule-provider.tsx
│ │ │ └── viewer.tsx
│ │ ├── rules
│ │ │ └── rule-item.tsx
│ │ ├── settings
│ │ │ ├── actions.tsx
│ │ │ ├── appearance-confis.tsx
│ │ │ ├── css-editor-modal.tsx
│ │ │ ├── general-config.tsx
│ │ │ ├── mihomo-config.tsx
│ │ │ ├── shortcut-config.tsx
│ │ │ ├── sider-config.tsx
│ │ │ ├── substore-config.tsx
│ │ │ ├── webdav-config.tsx
│ │ │ └── webdav-restore-modal.tsx
│ │ ├── sider
│ │ │ ├── config-viewer.tsx
│ │ │ ├── conn-card.tsx
│ │ │ ├── dns-card.tsx
│ │ │ ├── log-card.tsx
│ │ │ ├── mihomo-core-card.tsx
│ │ │ ├── outbound-mode-switcher.tsx
│ │ │ ├── override-card.tsx
│ │ │ ├── profile-card.tsx
│ │ │ ├── proxy-card.tsx
│ │ │ ├── resource-card.tsx
│ │ │ ├── rule-card.tsx
│ │ │ ├── sniff-card.tsx
│ │ │ ├── substore-card.tsx
│ │ │ ├── sysproxy-switcher.tsx
│ │ │ └── tun-switcher.tsx
│ │ ├── sysproxy
│ │ │ ├── bypass-editor-modal.tsx
│ │ │ └── pac-editor-modal.tsx
│ │ └── updater
│ │ │ ├── updater-button.tsx
│ │ │ └── updater-modal.tsx
│ │ ├── floating.tsx
│ │ ├── hooks
│ │ ├── use-app-config.tsx
│ │ ├── use-controled-mihomo-config.tsx
│ │ ├── use-groups.tsx
│ │ ├── use-override-config.tsx
│ │ ├── use-profile-config.tsx
│ │ └── use-rules.tsx
│ │ ├── main.tsx
│ │ ├── pages
│ │ ├── connections.tsx
│ │ ├── dns.tsx
│ │ ├── logs.tsx
│ │ ├── mihomo.tsx
│ │ ├── override.tsx
│ │ ├── profiles.tsx
│ │ ├── proxies.tsx
│ │ ├── resources.tsx
│ │ ├── rules.tsx
│ │ ├── settings.tsx
│ │ ├── sniffer.tsx
│ │ ├── substore.tsx
│ │ ├── syspeoxy.tsx
│ │ └── tun.tsx
│ │ ├── routes
│ │ └── index.tsx
│ │ └── utils
│ │ ├── calc.ts
│ │ ├── debounce.ts
│ │ ├── env.d.ts
│ │ ├── hash.ts
│ │ ├── includes.ts
│ │ ├── init.ts
│ │ └── ipc.ts
└── shared
│ └── types.d.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
└── tsconfig.web.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | out
4 | .gitignore
5 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'eslint:recommended',
4 | 'plugin:react/recommended',
5 | 'plugin:react/jsx-runtime',
6 | '@electron-toolkit/eslint-config-ts/recommended',
7 | '@electron-toolkit/eslint-config-prettier'
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report_zh.yml:
--------------------------------------------------------------------------------
1 | name: 错误反馈
2 | description: '提交 mihomo-party 漏洞'
3 | title: '[Bug] '
4 | body:
5 | - type: checkboxes
6 | id: ensure
7 | attributes:
8 | label: Verify steps
9 | description: 在提交之前,请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
10 | options:
11 | - label: 我已在标题简短的描述了我所遇到的问题
12 | - label: 我已在 [Issue Tracker](./?q=is%3Aissue) 中寻找过我要提出的问题,但未找到相同的问题
13 | - label: 我已在 [常见问题](https://mihomo.party/docs/issues/common) 中寻找过我要提出的问题,并没有找到答案
14 | - label: 这是 GUI 程序的问题,而不是内核程序的问题
15 | - label: 我已经关闭所有杀毒软件/代理软件后测试过,问题依旧存在
16 | - label: 我已经使用最新的测试版本测试过,问题依旧存在
17 |
18 | - type: dropdown
19 | attributes:
20 | label: 操作系统
21 | description: 请提供操作系统类型
22 | multiple: true
23 | options:
24 | - MacOS
25 | - Windows
26 | - Linux
27 | validations:
28 | required: true
29 | - type: input
30 | attributes:
31 | label: 系统版本
32 | description: 请提供出现问题的操作系统版本
33 | validations:
34 | required: true
35 | - type: input
36 | attributes:
37 | label: 发生问题 mihomo-party 版本
38 | validations:
39 | required: true
40 | - type: textarea
41 | attributes:
42 | label: 描述
43 | description: 请提供错误的详细描述。
44 | validations:
45 | required: true
46 | - type: textarea
47 | attributes:
48 | label: 重现方式
49 | description: 请提供重现错误的步骤
50 | validations:
51 | required: true
52 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: '常见问题'
4 | about: '提出问题前请先查看常见问题'
5 | url: 'https://mihomo.party/docs/issues/common'
6 | - name: '交流群组'
7 | about: '提问/讨论性质的问题请勿提交issue'
8 | url: 'https://t.me/mihomo_party_group'
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request_zh.yml:
--------------------------------------------------------------------------------
1 | name: 功能请求
2 | description: '请求 mihomo-party 功能'
3 | title: '[Feature] '
4 | body:
5 | - type: checkboxes
6 | id: ensure
7 | attributes:
8 | label: Verify steps
9 | description: 在提交之前,请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
10 | options:
11 | - label: 我已在标题简短的描述了我所需的功能
12 | - label: 我已在 [Issue Tracker](./?q=is%3Aissue) 中寻找过,但未找到我所需的功能
13 | - label: 这是向 GUI 程序提出的功能请求,而不是内核程序
14 | - label: 我未在最新的测试版本找到我所需的功能
15 |
16 | - type: dropdown
17 | attributes:
18 | label: 操作系统
19 | description: 请提供操作系统类型
20 | multiple: true
21 | options:
22 | - MacOS
23 | - Windows
24 | - Linux
25 | validations:
26 | required: true
27 | - type: textarea
28 | attributes:
29 | label: 描述
30 | description: 请提供所需功能的详细描述
31 | validations:
32 | required: true
33 |
--------------------------------------------------------------------------------
/.github/workflows/issues.yml:
--------------------------------------------------------------------------------
1 | name: Review Issues
2 |
3 | on:
4 | issues:
5 | types: [opened]
6 |
7 | jobs:
8 | review:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Generate Token
12 | uses: tibdex/github-app-token@v2
13 | id: generate
14 | with:
15 | app_id: ${{ secrets.BOT_APP_ID }}
16 | private_key: ${{ secrets.BOT_PRIVATE_KEY }}
17 | - name: Review Issues
18 | uses: mihomo-party-org/universal-assistant@v1.0.3
19 | with:
20 | github_token: ${{ steps.generate.outputs.token }}
21 | openai_base_url: ${{ secrets.OPENAI_BASE_URL }}
22 | openai_api_key: ${{ secrets.OPENAI_API_KEY }}
23 | openai_model: ${{ vars.OPENAI_MODEL }}
24 | system_prompt: ${{ vars.SYSTEM_PROMPT }}
25 | available_tools: ${{ vars.AVAILABLE_TOOLS }}
26 | user_input: |
27 | 请审查如下 Issue:
28 | 标题:"${{ github.event.issue.title }}"
29 | 内容:"${{ github.event.issue.body }}"
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | resources/files
3 | resources/sidecar
4 | extra
5 | dist
6 | out
7 | .DS_Store
8 | *.log*
9 | .idea
10 | *.ttf
11 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | virtual-store-dir-max-length=80
3 | public-hoist-pattern[]=*@heroui/*
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | pnpm-lock.yaml
4 | LICENSE.md
5 | tsconfig.json
6 | tsconfig.*.json
7 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | singleQuote: true
2 | semi: false
3 | printWidth: 100
4 | trailingComma: none
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Debug Main Process",
6 | "type": "node",
7 | "request": "launch",
8 | "cwd": "${workspaceRoot}",
9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
10 | "windows": {
11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
12 | },
13 | "runtimeArgs": ["--sourcemap"],
14 | "env": {
15 | "REMOTE_DEBUGGING_PORT": "9222"
16 | }
17 | },
18 | {
19 | "name": "Debug Renderer Process",
20 | "port": 9222,
21 | "request": "attach",
22 | "type": "chrome",
23 | "webRoot": "${workspaceFolder}/src/renderer",
24 | "timeout": 60000,
25 | "presentation": {
26 | "hidden": true
27 | }
28 | }
29 | ],
30 | "compounds": [
31 | {
32 | "name": "Debug All",
33 | "configurations": ["Debug Main Process", "Debug Renderer Process"],
34 | "presentation": {
35 | "order": 1
36 | }
37 | }
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[typescript]": {
3 | "editor.defaultFormatter": "esbenp.prettier-vscode"
4 | },
5 | "[typescriptreact]": {
6 | "editor.defaultFormatter": "esbenp.prettier-vscode"
7 | },
8 | "[javascript]": {
9 | "editor.defaultFormatter": "esbenp.prettier-vscode"
10 | },
11 | "[json]": {
12 | "editor.defaultFormatter": "esbenp.prettier-vscode"
13 | },
14 | "editor.formatOnSave": true
15 | }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sparkle
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ### 特性
15 |
16 | - [x] 开箱即用,无需服务模式的 Tun
17 | - [x] 多种配色主题可选,UI 焕然一新
18 | - [x] 支持大部分 Mihomo 常用配置修改
19 | - [x] 内置稳定版和预览版 Mihomo 内核
20 | - [x] 通过 WebDAV 一键备份和恢复配置
21 | - [x] 强大的覆写功能,任意修订配置文件
22 | - [x] 深度集成 Sub-Store,轻松管理订阅
23 |
--------------------------------------------------------------------------------
/aur/sparkle-bin/PKGBUILD:
--------------------------------------------------------------------------------
1 | pkgname=sparkle-bin
2 | _pkgname=sparkle
3 | pkgver=1.6.2
4 | pkgrel=1
5 | pkgdesc="Another Mihomo GUI."
6 | arch=('x86_64' 'aarch64')
7 | url="https://github.com/xishang0128/sparkle"
8 | license=('GPL3')
9 | conflicts=("$_pkgname" "$_pkgname-git" "$_pkgname-electron" "$_pkgname-electron-bin" "$_pkgname-electron-git")
10 | conflicts=("sparkle-git" 'sparkle')
11 | depends=('gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
12 | optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
13 | install=$_pkgname.install
14 | source=("${_pkgname}.sh")
15 | source_x86_64=("${_pkgname}-${pkgver}-x86_64.deb::${url}/releases/download/${pkgver}/sparkle-linux-${pkgver}-amd64.deb")
16 | source_aarch64=("${_pkgname}-${pkgver}-aarch64.deb::${url}/releases/download/${pkgver}/sparkle-linux-${pkgver}-arm64.deb")
17 | sha256sums=('03eb601fe981716e90f9170eeb36a2e7938587f05a1bdaa09adadb1229c77a0a')
18 | sha256sums_x86_64=('b8d166f1134573336aaae1866d25262284b0cbabbf393684226aca0fd8d1bd83')
19 | sha256sums_aarch64=('8cd7398b8fc1cd70d41e386af9995cbddc1043d9018391c29f056f1435712a10')
20 |
21 | package() {
22 | bsdtar -xf data.tar.xz -C "${pkgdir}/"
23 | chmod +x ${pkgdir}/opt/sparkle/sparkle
24 | chmod +x ${pkgdir}/opt/sparkle/resources/files/sysproxy
25 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
26 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
27 | install -Dm755 "${srcdir}/${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
28 | sed -i '3s!/opt/sparkle/sparkle!sparkle!' "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
29 |
30 | chown -R root:root ${pkgdir}
31 | }
32 |
--------------------------------------------------------------------------------
/aur/sparkle-bin/sparkle.install:
--------------------------------------------------------------------------------
1 | # Colored makepkg-like functions
2 | note() {
3 | printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
4 | }
5 |
6 | _all_off="$(tput sgr0)"
7 | _bold="${_all_off}$(tput bold)"
8 | _blue="${_bold}$(tput setaf 4)"
9 | _yellow="${_bold}$(tput setaf 3)"
10 |
11 | post_install() {
12 | note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
13 | note "The launcher is called: 'sparkle'"
14 | }
--------------------------------------------------------------------------------
/aur/sparkle-bin/sparkle.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}
4 |
5 | # Allow users to override command-line options
6 | if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
7 | mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
8 | echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
9 | fi
10 |
11 | # Launch
12 | exec /opt/sparkle/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"
13 |
--------------------------------------------------------------------------------
/aur/sparkle-electron-bin/PKGBUILD:
--------------------------------------------------------------------------------
1 | pkgname=sparkle-electron-bin
2 | _pkgname=sparkle
3 | pkgver=1.6.2
4 | pkgrel=1
5 | pkgdesc="Another Mihomo GUI."
6 | arch=('x86_64' 'aarch64')
7 | url="https://github.com/xishang0128/sparkle"
8 | license=('GPL3')
9 | conflicts=("$_pkgname" "$_pkgname-git" "$_pkgname-bin" "$_pkgname-electron" "$_pkgname-electron-git")
10 | depends=('electron' 'gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
11 | optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
12 | makedepends=('asar')
13 | install=$_pkgname.install
14 | source=("${_pkgname}.desktop" "${_pkgname}.sh")
15 | source_x86_64=("${_pkgname}-${pkgver}-x86_64.deb::${url}/releases/download/${pkgver}/sparkle-linux-${pkgver}-amd64.deb")
16 | source_aarch64=("${_pkgname}-${pkgver}-aarch64.deb::${url}/releases/download/${pkgver}/sparkle-linux-${pkgver}-arm64.deb")
17 | sha256sums=(
18 | "b17d85f6d862285a53a24d0f8dedd08f1f3c852ba6a901fabc487177598803cc"
19 | "ce855656fb0682d403685244c77dd2d90ec6efb207753fb7a6ddc1e9b6aa2c49"
20 | )
21 | sha256sums_x86_64=("43f8b9a5818a722cdb8e5044d2a90993274860b0da96961e1a2652169539ce39")
22 | sha256sums_aarch64=("18574fdeb01877a629aa52ac0175335ce27c83103db4fcb2f1ad69e3e42ee10f")
23 | options=('!lto')
24 |
25 | package() {
26 | bsdtar -xf data.tar.xz -C $srcdir
27 | asar extract $srcdir/opt/sparkle/resources/app.asar ${pkgdir}/opt/sparkle
28 | cp -r $srcdir/opt/sparkle/resources/sidecar ${pkgdir}/opt/sparkle/resources/
29 | cp -r $srcdir/opt/sparkle/resources/files ${pkgdir}/opt/sparkle/resources/
30 | chmod +x ${pkgdir}/opt/sparkle/resources/files/sysproxy
31 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
32 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
33 | install -Dm755 "${srcdir}/${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
34 | install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
35 | install -Dm644 "${pkgdir}/opt/sparkle/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"
36 |
37 | chown -R root:root ${pkgdir}
38 | }
39 |
--------------------------------------------------------------------------------
/aur/sparkle-electron-bin/sparkle.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Sparkle
3 | Exec=sparkle %U
4 | Terminal=false
5 | Type=Application
6 | Icon=sparkle
7 | StartupWMClass=sparkle
8 | MimeType=x-scheme-handler/clash;x-scheme-handler/mihomo;
9 | Comment=Sparkle
10 | Categories=Utility;
11 |
--------------------------------------------------------------------------------
/aur/sparkle-electron-bin/sparkle.install:
--------------------------------------------------------------------------------
1 | # Colored makepkg-like functions
2 | note() {
3 | printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
4 | }
5 |
6 | _all_off="$(tput sgr0)"
7 | _bold="${_all_off}$(tput bold)"
8 | _blue="${_bold}$(tput setaf 4)"
9 | _yellow="${_bold}$(tput setaf 3)"
10 |
11 | post_install() {
12 | note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
13 | note "The launcher is called: 'sparkle'"
14 | }
--------------------------------------------------------------------------------
/aur/sparkle-electron-bin/sparkle.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}
4 |
5 | # Allow users to override command-line options
6 | if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
7 | mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
8 | echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
9 | fi
10 |
11 | # Launch
12 | exec electron /opt/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"
13 |
--------------------------------------------------------------------------------
/aur/sparkle-electron-git/PKGBUILD:
--------------------------------------------------------------------------------
1 | pkgname=sparkle-electron-git
2 | _pkgname=${pkgname%-electron-git}
3 | pkgver=r737.e4a7e67
4 | pkgrel=1
5 | pkgdesc="Another Mihomo GUI."
6 | arch=('x86_64' 'aarch64')
7 | url="https://github.com/xishang0128/sparkle"
8 | license=('GPL3')
9 | conflicts=("$_pkgname" "$_pkgname-git" "$_pkgname-bin" "$_pkgname-electron" "$_pkgname-electron-bin")
10 | depends=('electron' 'gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
11 | optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
12 | makedepends=('nodejs' 'pnpm' 'libxcrypt-compat' 'asar')
13 | install=$_pkgname.install
14 | source=(
15 | "${_pkgname}.desktop"
16 | "${_pkgname}.sh"
17 | "git+$url.git"
18 | )
19 | sha256sums=(
20 | "b17d85f6d862285a53a24d0f8dedd08f1f3c852ba6a901fabc487177598803cc"
21 | "ce855656fb0682d403685244c77dd2d90ec6efb207753fb7a6ddc1e9b6aa2c49"
22 | "SKIP"
23 | )
24 | options=('!lto')
25 |
26 | pkgver() {
27 | cd $srcdir/${_pkgname}
28 | ( set -o pipefail
29 | git describe --long 2>/dev/null | sed 's/\([^-]*-g\)/r\1/;s/-/./g' | tr -d 'v' ||
30 | printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
31 | )
32 | }
33 |
34 | prepare(){
35 | cd $srcdir/${_pkgname}
36 | sed -i "s/productName: Sparkle/productName: sparkle/" electron-builder.yml
37 | pnpm install
38 | }
39 |
40 | build(){
41 | cd $srcdir/${_pkgname}
42 | pnpm build:linux deb
43 | }
44 |
45 | package() {
46 | asar extract $srcdir/${_pkgname}/dist/linux-unpacked/resources/app.asar ${pkgdir}/opt/sparkle
47 | cp -r $srcdir/${_pkgname}/extra/sidecar ${pkgdir}/opt/sparkle/resources/
48 | cp -r $srcdir/${_pkgname}/extra/files ${pkgdir}/opt/sparkle/resources/
49 | chmod +x ${pkgdir}/opt/sparkle/resources/files/sysproxy
50 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
51 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
52 | install -Dm755 "${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
53 | install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
54 | install -Dm644 "${pkgdir}/opt/sparkle/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"
55 |
56 | chown -R root:root ${pkgdir}
57 | }
58 |
--------------------------------------------------------------------------------
/aur/sparkle-electron-git/sparkle.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Sparkle
3 | Exec=sparkle %U
4 | Terminal=false
5 | Type=Application
6 | Icon=sparkle
7 | StartupWMClass=sparkle
8 | MimeType=x-scheme-handler/clash;x-scheme-handler/mihomo;
9 | Comment=Sparkle
10 | Categories=Utility;
11 |
--------------------------------------------------------------------------------
/aur/sparkle-electron-git/sparkle.install:
--------------------------------------------------------------------------------
1 | # Colored makepkg-like functions
2 | note() {
3 | printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
4 | }
5 |
6 | _all_off="$(tput sgr0)"
7 | _bold="${_all_off}$(tput bold)"
8 | _blue="${_bold}$(tput setaf 4)"
9 | _yellow="${_bold}$(tput setaf 3)"
10 |
11 | post_install() {
12 | note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
13 | note "The launcher is called: 'sparkle'"
14 | }
--------------------------------------------------------------------------------
/aur/sparkle-electron-git/sparkle.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}
4 |
5 | # Allow users to override command-line options
6 | if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
7 | mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
8 | echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
9 | fi
10 |
11 | # Launch
12 | exec electron /opt/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"
13 |
--------------------------------------------------------------------------------
/aur/sparkle-electron/PKGBUILD:
--------------------------------------------------------------------------------
1 | pkgname=sparkle-electron
2 | _pkgname=sparkle
3 | pkgver=1.6.2
4 | pkgrel=1
5 | pkgdesc="Another Mihomo GUI."
6 | arch=('x86_64' 'aarch64')
7 | url=" "
8 | license=('GPL3')
9 | conflicts=("$_pkgname" "$_pkgname-git" "$_pkgname-bin" "$_pkgname-electron-bin" "$_pkgname-electron-git")
10 | depends=('electron' 'gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
11 | optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
12 | makedepends=('nodejs' 'pnpm' 'libxcrypt-compat' 'asar')
13 | install=$_pkgname.install
14 | source=(
15 | "${_pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/${pkgver}.tar.gz"
16 | "${_pkgname}.desktop"
17 | "${_pkgname}.sh"
18 | )
19 | sha256sums=("d2fe3633951f7e164bc2df4437decd86e880a516e318363601ea552989c0c73d"
20 | "b17d85f6d862285a53a24d0f8dedd08f1f3c852ba6a901fabc487177598803cc"
21 | "ce855656fb0682d403685244c77dd2d90ec6efb207753fb7a6ddc1e9b6aa2c49"
22 | )
23 | options=('!lto')
24 |
25 | prepare(){
26 | cd $srcdir/${_pkgname}-${pkgver}
27 | sed -i "s/productName: Sparkle/productName: sparkle/" electron-builder.yml
28 | pnpm install
29 | }
30 |
31 | build(){
32 | cd $srcdir/${_pkgname}-${pkgver}
33 | pnpm build:linux deb
34 | }
35 |
36 | package() {
37 | asar extract $srcdir/${_pkgname}-${pkgver}/dist/linux-unpacked/resources/app.asar ${pkgdir}/opt/sparkle
38 | cp -r $srcdir/${_pkgname}-${pkgver}/extra/sidecar ${pkgdir}/opt/sparkle/resources/
39 | cp -r $srcdir/${_pkgname}-${pkgver}/extra/files ${pkgdir}/opt/sparkle/resources/
40 | chmod +x ${pkgdir}/opt/sparkle/resources/files/sysproxy
41 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
42 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
43 | install -Dm755 "${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
44 | install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
45 | install -Dm644 "${pkgdir}/opt/sparkle/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"
46 |
47 | chown -R root:root ${pkgdir}
48 | }
49 |
--------------------------------------------------------------------------------
/aur/sparkle-electron/sparkle.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Sparkle
3 | Exec=sparkle %U
4 | Terminal=false
5 | Type=Application
6 | Icon=sparkle
7 | StartupWMClass=sparkle
8 | MimeType=x-scheme-handler/clash;x-scheme-handler/mihomo;
9 | Comment=Sparkle
10 | Categories=Utility;
11 |
--------------------------------------------------------------------------------
/aur/sparkle-electron/sparkle.install:
--------------------------------------------------------------------------------
1 | # Colored makepkg-like functions
2 | note() {
3 | printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
4 | }
5 |
6 | _all_off="$(tput sgr0)"
7 | _bold="${_all_off}$(tput bold)"
8 | _blue="${_bold}$(tput setaf 4)"
9 | _yellow="${_bold}$(tput setaf 3)"
10 |
11 | post_install() {
12 | note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
13 | note "The launcher is called: 'sparkle'"
14 | }
--------------------------------------------------------------------------------
/aur/sparkle-electron/sparkle.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}
4 |
5 | # Allow users to override command-line options
6 | if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
7 | mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
8 | echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
9 | fi
10 |
11 | # Launch
12 | exec electron /opt/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"
13 |
--------------------------------------------------------------------------------
/aur/sparkle-git/PKGBUILD:
--------------------------------------------------------------------------------
1 | pkgname=sparkle-git
2 | _pkgname=${pkgname%-git}
3 | pkgver=1.6.2.r1.db8c6a0
4 | pkgrel=1
5 | pkgdesc="Another Mihomo GUI."
6 | arch=('x86_64' 'aarch64')
7 | url="https://github.com/xishang0128/sparkle"
8 | license=('GPL3')
9 | conflicts=("$_pkgname" "$_pkgname-bin" "$_pkgname-electron" "$_pkgname-electron-bin" "$_pkgname-electron-git")
10 | depends=('gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
11 | optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
12 | makedepends=('nodejs' 'pnpm' 'jq' 'libxcrypt-compat')
13 | install=$_pkgname.install
14 | source=("${_pkgname}.sh" "git+$url.git")
15 | sha256sums=("03eb601fe981716e90f9170eeb36a2e7938587f05a1bdaa09adadb1229c77a0a" "SKIP")
16 | options=('!lto')
17 |
18 | pkgver() {
19 | cd $srcdir/${_pkgname}
20 | ( set -o pipefail
21 | git describe --long 2>/dev/null | sed 's/\([^-]*-g\)/r\1/;s/-/./g' | tr -d 'v' ||
22 | printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
23 | )
24 | }
25 |
26 | prepare(){
27 | cd $srcdir/${_pkgname}
28 | sed -i "s/productName: Sparkle/productName: sparkle/" electron-builder.yml
29 | pnpm install
30 | }
31 |
32 | build(){
33 | cd $srcdir/${_pkgname}
34 | pnpm build:linux deb
35 | }
36 |
37 | package() {
38 | cd $srcdir/${_pkgname}/dist
39 | bsdtar -xf sparkle-linux-$(jq '.version' $srcdir/${_pkgname}/package.json | tr -d 'v"')*.deb
40 | bsdtar -xf data.tar.xz -C "${pkgdir}/"
41 | chmod +x ${pkgdir}/opt/sparkle/sparkle
42 | chmod +x ${pkgdir}/opt/sparkle/resources/files/sysproxy
43 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
44 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
45 | install -Dm755 "${srcdir}/../${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
46 | sed -i '3s!/opt/sparkle/sparkle!sparkle!' "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
47 |
48 | chown -R root:root ${pkgdir}
49 | }
50 |
--------------------------------------------------------------------------------
/aur/sparkle-git/sparkle.install:
--------------------------------------------------------------------------------
1 | # Colored makepkg-like functions
2 | note() {
3 | printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
4 | }
5 |
6 | _all_off="$(tput sgr0)"
7 | _bold="${_all_off}$(tput bold)"
8 | _blue="${_bold}$(tput setaf 4)"
9 | _yellow="${_bold}$(tput setaf 3)"
10 |
11 | post_install() {
12 | note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
13 | note "The launcher is called: 'sparkle'"
14 | }
--------------------------------------------------------------------------------
/aur/sparkle-git/sparkle.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}
4 |
5 | # Allow users to override command-line options
6 | if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
7 | mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
8 | echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
9 | fi
10 |
11 | # Launch
12 | exec /opt/sparkle/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"
13 |
--------------------------------------------------------------------------------
/aur/sparkle/PKGBUILD:
--------------------------------------------------------------------------------
1 | pkgname=sparkle
2 | pkgver=1.6.2
3 | pkgrel=1
4 | pkgdesc="Another Mihomo GUI."
5 | arch=('x86_64' 'aarch64')
6 | url="https://github.com/xishang0128/sparkle"
7 | license=('GPL3')
8 | conflicts=("$pkgname-git" "$pkgname-bin" "$pkgname-electron" "$pkgname-electron-bin" "$_pkgname-electron-git")
9 | depends=('gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
10 | optdepends=('libappindicator-gtk3: Allow sparkle to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
11 | makedepends=('nodejs' 'pnpm' 'libxcrypt-compat')
12 | install=$pkgname.install
13 | source=(
14 | "${pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/${pkgver}.tar.gz"
15 | "${pkgname}.sh"
16 | )
17 | sha256sums=("d2fe3633951f7e164bc2df4437decd86e880a516e318363601ea552989c0c73d"
18 | "03eb601fe981716e90f9170eeb36a2e7938587f05a1bdaa09adadb1229c77a0a")
19 | options=('!lto')
20 |
21 | prepare(){
22 | cd $srcdir/${pkgname}-${pkgver}
23 | sed -i "s/productName: Sparkle/productName: sparkle/" electron-builder.yml
24 | pnpm install
25 | }
26 |
27 | build(){
28 | cd $srcdir/${pkgname}-${pkgver}
29 | pnpm build:linux deb
30 | }
31 |
32 | package() {
33 | cd $srcdir/${pkgname}-${pkgver}/dist
34 | bsdtar -xf sparkle-linux-${pkgver}*.deb
35 | bsdtar -xf data.tar.xz -C "${pkgdir}/"
36 | chmod +x ${pkgdir}/opt/sparkle/sparkle
37 | chmod +x ${pkgdir}/opt/sparkle/resources/files/sysproxy
38 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo
39 | chmod +sx ${pkgdir}/opt/sparkle/resources/sidecar/mihomo-alpha
40 | install -Dm755 "${srcdir}/../${pkgname}.sh" "${pkgdir}/usr/bin/${pkgname}"
41 | sed -i '3s!/opt/sparkle/sparkle!sparkle!' "${pkgdir}/usr/share/applications/${pkgname}.desktop"
42 |
43 | chown -R root:root ${pkgdir}
44 | }
45 |
--------------------------------------------------------------------------------
/aur/sparkle/sparkle.install:
--------------------------------------------------------------------------------
1 | # Colored makepkg-like functions
2 | note() {
3 | printf "${_blue}==>${_yellow} NOTE:${_bold} %s${_all_off}\n" "$1"
4 | }
5 |
6 | _all_off="$(tput sgr0)"
7 | _bold="${_all_off}$(tput bold)"
8 | _blue="${_bold}$(tput setaf 4)"
9 | _yellow="${_bold}$(tput setaf 3)"
10 |
11 | post_install() {
12 | note "Custom flags should be put directly in: ~/.config/sparkle-flags.conf"
13 | note "The launcher is called: 'sparkle'"
14 | }
--------------------------------------------------------------------------------
/aur/sparkle/sparkle.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-~/.config}
4 |
5 | # Allow users to override command-line options
6 | if [[ -f "${XDG_CONFIG_HOME}/sparkle-flags.conf" ]]; then
7 | mapfile -t MIHOMO_PARTY_USER_FLAGS <<<"$(grep -v '^#' "${XDG_CONFIG_HOME}/sparkle-flags.conf")"
8 | echo "User flags:" ${MIHOMO_PARTY_USER_FLAGS[@]}
9 | fi
10 |
11 | # Launch
12 | exec /opt/sparkle/sparkle ${MIHOMO_PARTY_USER_FLAGS[@]} "$@"
13 |
--------------------------------------------------------------------------------
/build/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-jit
6 |
7 | com.apple.security.cs.allow-unsigned-executable-memory
8 |
9 | com.apple.security.cs.allow-dyld-environment-variables
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/build/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xishang0128/sparkle/61a263d09c3915353720b1162d97e18f701b82d9/build/icon.icns
--------------------------------------------------------------------------------
/build/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xishang0128/sparkle/61a263d09c3915353720b1162d97e18f701b82d9/build/icon.ico
--------------------------------------------------------------------------------
/build/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xishang0128/sparkle/61a263d09c3915353720b1162d97e18f701b82d9/build/icon.png
--------------------------------------------------------------------------------
/build/installerIcon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xishang0128/sparkle/61a263d09c3915353720b1162d97e18f701b82d9/build/installerIcon.ico
--------------------------------------------------------------------------------
/build/linux/postinst:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if type update-alternatives 2>/dev/null >&1; then
4 | # Remove previous link if it doesn't use update-alternatives
5 | if [ -L '/usr/bin/sparkle' -a -e '/usr/bin/sparkle' -a "`readlink '/usr/bin/sparkle'`" != '/etc/alternatives/sparkle' ]; then
6 | rm -f '/usr/bin/sparkle'
7 | fi
8 | update-alternatives --install '/usr/bin/sparkle' 'sparkle' '/opt/sparkle/sparkle' 100 || ln -sf '/opt/sparkle/sparkle' '/usr/bin/sparkle'
9 | else
10 | ln -sf '/opt/sparkle/sparkle' '/usr/bin/sparkle'
11 | fi
12 |
13 | sed -i 's/Name=sparkle/Name=Sparkle/' '/usr/share/applications/sparkle.desktop'
14 |
15 | chmod 4755 '/opt/sparkle/chrome-sandbox' || true
16 | chmod +x /opt/sparkle/resources/files/sysproxy
17 | chmod +sx /opt/sparkle/resources/sidecar/mihomo
18 | chmod +sx /opt/sparkle/resources/sidecar/mihomo-alpha
19 |
20 | if hash update-mime-database 2>/dev/null; then
21 | update-mime-database /usr/share/mime || true
22 | fi
23 |
24 | if hash update-desktop-database 2>/dev/null; then
25 | update-desktop-database /usr/share/applications || true
26 | fi
27 |
--------------------------------------------------------------------------------
/build/pkg-scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | chown root:admin $2/Sparkle.app/Contents/Resources/sidecar/mihomo
3 | chown root:admin $2/Sparkle.app/Contents/Resources/sidecar/mihomo-alpha
4 | chmod +s $2/Sparkle.app/Contents/Resources/sidecar/mihomo
5 | chmod +s $2/Sparkle.app/Contents/Resources/sidecar/mihomo-alpha
6 |
7 | mkdir -p /Library/PrivilegedHelperTools
8 | cp $2/Sparkle.app/Contents/Resources/files/sysproxy /Library/PrivilegedHelperTools/sparkle.helper
9 | chown root:wheel /Library/PrivilegedHelperTools/sparkle.helper
10 | chmod 544 /Library/PrivilegedHelperTools/sparkle.helper
11 | cat << EOF > /Library/LaunchDaemons/sparkle.helper.plist
12 |
13 |
14 |
15 |
16 | Label
17 | sparkle.helper
18 | MachServices
19 |
20 | sparkle.helper
21 |
22 |
23 | KeepAlive
24 |
25 | Program
26 | /Library/PrivilegedHelperTools/sparkle.helper
27 | ProgramArguments
28 |
29 | /Library/PrivilegedHelperTools/sparkle.helper
30 | server
31 |
32 | StandardErrorPath
33 | /tmp/sparkle.helper.err
34 | StandardOutPath
35 | /tmp/sparkle.helper.log
36 |
37 |
38 | EOF
39 | chown root:wheel /Library/LaunchDaemons/sparkle.helper.plist
40 | chmod 644 /Library/LaunchDaemons/sparkle.helper.plist
41 | launchctl unload /Library/LaunchDaemons/sparkle.helper.plist
42 | launchctl load /Library/LaunchDaemons/sparkle.helper.plist
43 | launchctl start sparkle.helper
44 | exit 0
--------------------------------------------------------------------------------
/build/pkg-scripts/preinstall:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ -e "/Library/LaunchDaemons/sparkle.helper.plist" ]; then
4 | launchctl unload /Library/LaunchDaemons/sparkle.helper.plist
5 | rm /Library/LaunchDaemons/sparkle.helper.plist
6 | fi
7 |
8 | if [ -e "/Library/PrivilegedHelperTools/sparkle.helper" ]; then
9 | rm /Library/PrivilegedHelperTools/sparkle.helper
10 | fi
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | ### Breaking Changes
2 |
3 | - 1.5.0 之后 macOS 改用 pkg 安装方式,不再支持 dmg 安装方式,因此本次更新需要手动下载安装包进行安装
4 | - electron33 已不再支持 macOS 10.15,故为 10.15 提供单独的安装包,需要的用户请自行下载安装,应用内更新时会自动检测系统版本,安装后后续可正常在应用内直接更新
5 | - 1.5.1 之后 Windows 下 `productName` 改为 `Mihomo Party`, 更新后若出现找不到文件报错,手动以管理员权限运行 `Mihomo Party.exe` 即可
6 | - 由于更改了应用名称,开机启动失效是正常现象,在设置中重新开关一下即可
7 |
8 | ### Features
9 |
10 | - 添加出站接口查看
11 | - 添加更多嗅探配置
12 |
13 | ### Bug Fixes
14 |
15 | - null
16 |
--------------------------------------------------------------------------------
/electron-builder.yml:
--------------------------------------------------------------------------------
1 | appId: sparkle.app
2 | productName: Sparkle
3 | directories:
4 | buildResources: build
5 | files:
6 | - '!**/.vscode/*'
7 | - '!src/*'
8 | - '!aur/*'
9 | - '!images/*'
10 | - '!scripts/*'
11 | - '!extra/*'
12 | - '!tailwind.config.js'
13 | - '!postcss.config.js'
14 | - '!electron.vite.config.{js,ts,mjs,cjs}'
15 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
16 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
17 | - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
18 | extraResources:
19 | - from: './extra/'
20 | to: ''
21 | protocols:
22 | name: 'Sparkle URI Scheme'
23 | schemes:
24 | - 'clash'
25 | - 'mihomo'
26 | win:
27 | target:
28 | - nsis
29 | - 7z
30 | artifactName: ${name}-windows-${version}-${arch}-portable.${ext}
31 | nsis:
32 | artifactName: ${name}-windows-${version}-${arch}-setup.${ext}
33 | uninstallDisplayName: ${productName}
34 | allowToChangeInstallationDirectory: true
35 | oneClick: false
36 | perMachine: true
37 | createDesktopShortcut: true
38 | mac:
39 | target:
40 | - pkg
41 | entitlementsInherit: build/entitlements.mac.plist
42 | extendInfo:
43 | - NSCameraUsageDescription: Application requests access to the device's camera.
44 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
45 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
46 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
47 | notarize: false
48 | artifactName: ${name}-macos-${version}-${arch}.${ext}
49 | pkg:
50 | allowAnywhere: false
51 | allowCurrentUserHome: false
52 | # background:
53 | # alignment: bottomleft
54 | # file: build/background.png
55 | linux:
56 | desktop:
57 | entry:
58 | Name: Sparkle
59 | MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo'
60 | target:
61 | - deb
62 | - rpm
63 | - pacman
64 | maintainer: xishang0128
65 | category: Utility
66 | artifactName: ${name}-linux-${version}-${arch}.${ext}
67 | deb:
68 | afterInstall: 'build/linux/postinst'
69 | rpm:
70 | afterInstall: 'build/linux/postinst'
71 | pacman:
72 | afterInstall: 'build/linux/postinst'
73 | artifactName: ${name}-linux-${version}-${arch}.pkg.tar.xz
74 | npmRebuild: true
75 | publish: []
76 |
--------------------------------------------------------------------------------
/electron.vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
3 | import react from '@vitejs/plugin-react'
4 | // https://github.com/vdesjs/vite-plugin-monaco-editor/issues/21#issuecomment-1827562674
5 | import monacoEditorPluginModule from 'vite-plugin-monaco-editor'
6 | const isObjectWithDefaultFunction = (
7 | module: unknown
8 | ): module is { default: typeof monacoEditorPluginModule } =>
9 | module != null &&
10 | typeof module === 'object' &&
11 | 'default' in module &&
12 | typeof module.default === 'function'
13 | const monacoEditorPlugin = isObjectWithDefaultFunction(monacoEditorPluginModule)
14 | ? monacoEditorPluginModule.default
15 | : monacoEditorPluginModule
16 |
17 | export default defineConfig({
18 | main: {
19 | plugins: [externalizeDepsPlugin()]
20 | },
21 | preload: {
22 | plugins: [externalizeDepsPlugin()]
23 | },
24 | renderer: {
25 | build: {
26 | rollupOptions: {
27 | input: {
28 | index: resolve('src/renderer/index.html'),
29 | floating: resolve('src/renderer/floating.html')
30 | }
31 | }
32 | },
33 | resolve: {
34 | alias: {
35 | '@renderer': resolve('src/renderer/src')
36 | }
37 | },
38 | plugins: [
39 | react(),
40 | monacoEditorPlugin({
41 | languageWorkers: ['editorWorkerService', 'typescript', 'css'],
42 | customDistPath: (_, out) => `${out}/monacoeditorwork`,
43 | customWorkers: [
44 | {
45 | label: 'yaml',
46 | entry: 'monaco-yaml/yaml.worker'
47 | }
48 | ]
49 | })
50 | ]
51 | }
52 | })
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sparkle",
3 | "version": "1.6.6",
4 | "description": "Sparkle",
5 | "main": "./out/main/index.js",
6 | "author": "xishang0128",
7 | "homepage": "https://github.com/xishang0128/sparkle",
8 | "scripts": {
9 | "format": "prettier --write .",
10 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
11 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
12 | "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
13 | "typecheck": "npm run typecheck:node && npm run typecheck:web",
14 | "prepare": "node scripts/prepare.mjs",
15 | "updater": "node scripts/updater.mjs",
16 | "checksum": "node scripts/checksum.mjs",
17 | "telegram": "node scripts/telegram.mjs",
18 | "artifact": "node scripts/artifact.mjs",
19 | "dev": "electron-vite dev",
20 | "postinstall": "electron-builder install-app-deps",
21 | "build:win": "electron-vite build && electron-builder --publish never --win",
22 | "build:mac": "electron-vite build && electron-builder --publish never --mac",
23 | "build:linux": "electron-vite build && electron-builder --publish never --linux"
24 | },
25 | "dependencies": {
26 | "@electron-toolkit/preload": "^3.0.2",
27 | "@electron-toolkit/utils": "^4.0.0",
28 | "@heroui/react": "^2.6.13",
29 | "@types/crypto-js": "^4.2.2",
30 | "adm-zip": "^0.5.16",
31 | "axios": "^1.9.0",
32 | "chokidar": "^4.0.1",
33 | "crypto-js": "^4.2.0",
34 | "dayjs": "^1.11.13",
35 | "express": "^5.1.0",
36 | "iconv-lite": "^0.6.3",
37 | "webdav": "^5.8.0",
38 | "ws": "^8.18.2",
39 | "yaml": "^2.8.0"
40 | },
41 | "devDependencies": {
42 | "@dnd-kit/core": "^6.1.0",
43 | "@dnd-kit/sortable": "^10.0.0",
44 | "@dnd-kit/utilities": "^3.2.2",
45 | "@electron-toolkit/eslint-config-prettier": "^3.0.0",
46 | "@electron-toolkit/eslint-config-ts": "^3.1.0",
47 | "@electron-toolkit/tsconfig": "^1.0.1",
48 | "@types/adm-zip": "^0.5.6",
49 | "@types/express": "^5.0.2",
50 | "@types/node": "^22.15.24",
51 | "@types/pubsub-js": "^1.8.6",
52 | "@types/react": "^19.1.6",
53 | "@types/react-dom": "^19.1.5",
54 | "@types/ws": "^8.18.1",
55 | "@vitejs/plugin-react": "^4.5.0",
56 | "autoprefixer": "^10.4.21",
57 | "cron-validator": "^1.3.1",
58 | "driver.js": "^1.3.6",
59 | "electron": "^36.3.2",
60 | "electron-builder": "26.0.12",
61 | "electron-vite": "^3.1.0",
62 | "electron-window-state": "^5.0.3",
63 | "eslint": "9.26.0",
64 | "eslint-plugin-react": "^7.37.5",
65 | "form-data": "^4.0.2",
66 | "framer-motion": "12.11.0",
67 | "lodash": "^4.17.21",
68 | "meta-json-schema": "^1.19.9",
69 | "monaco-yaml": "^5.4.0",
70 | "nanoid": "^5.1.5",
71 | "next-themes": "^0.4.6",
72 | "postcss": "^8.5.4",
73 | "prettier": "^3.5.3",
74 | "pubsub-js": "^1.9.5",
75 | "react": "^19.1.0",
76 | "react-dom": "^19.1.0",
77 | "react-error-boundary": "^6.0.0",
78 | "react-icons": "^5.5.0",
79 | "react-markdown": "^10.1.0",
80 | "react-monaco-editor": "^0.58.0",
81 | "react-router-dom": "^7.6.1",
82 | "react-virtuoso": "^4.12.7",
83 | "recharts": "^2.15.3",
84 | "swr": "^2.3.3",
85 | "tailwindcss": "^3.4.14",
86 | "tar": "^7.4.3",
87 | "tsx": "^4.19.4",
88 | "types-pac": "^1.0.3",
89 | "typescript": "^5.8.3",
90 | "vite": "^6.3.5",
91 | "vite-plugin-monaco-editor": "^1.1.0"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/resources/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xishang0128/sparkle/61a263d09c3915353720b1162d97e18f701b82d9/resources/icon.ico
--------------------------------------------------------------------------------
/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xishang0128/sparkle/61a263d09c3915353720b1162d97e18f701b82d9/resources/icon.png
--------------------------------------------------------------------------------
/resources/iconTemplate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xishang0128/sparkle/61a263d09c3915353720b1162d97e18f701b82d9/resources/iconTemplate.png
--------------------------------------------------------------------------------
/resources/subStoreIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xishang0128/sparkle/61a263d09c3915353720b1162d97e18f701b82d9/resources/subStoreIcon.png
--------------------------------------------------------------------------------
/scripts/checksum.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync, readdirSync, writeFileSync } from 'fs'
2 | import { createHash } from 'crypto'
3 | const files = readdirSync('dist')
4 |
5 | for (const file of files) {
6 | for (const ext of process.argv.slice(2)) {
7 | if (file.endsWith(ext)) {
8 | const content = readFileSync(`dist/${file}`)
9 | const checksum = createHash('sha256').update(content, 'utf8').digest('hex')
10 | writeFileSync(`dist/${file}.sha256`, checksum)
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/scripts/telegram.mjs:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { readFileSync } from 'fs'
3 |
4 | const chat_id = '@MihomoPartyChannel'
5 | const pkg = readFileSync('package.json', 'utf-8')
6 | const changelog = readFileSync('changelog.md', 'utf-8')
7 | const { version } = JSON.parse(pkg)
8 | const downloadUrl = `https://github.com/mihomo-party-org/mihomo-party/releases/download/v${version}`
9 | let content = `🌟 Mihomo Party v${version} 正式发布\n\n`
10 | for (const line of changelog.split('\n')) {
11 | if (line.length === 0) {
12 | content += '\n'
13 | } else if (line.startsWith('### ')) {
14 | content += `${line.replace('### ', '')}\n`
15 | } else {
16 | content += `${line}\n`
17 | }
18 | }
19 |
20 | content += '\n下载地址:\nWindows10/11:\n'
21 | content += `安装版:64位 | 32位 | ARM64\n`
22 | content += `便携版:64位 | 32位 | ARM64\n`
23 | content += '\nWindows7/8:\n'
24 | content += `安装版:64位 | 32位\n`
25 | content += `便携版:64位 | 32位\n`
26 | content += '\nmacOS 11+:\n'
27 | content += `PKG:Intel | Apple Silicon\n`
29 | content += '\nmacOS 10.15+:\n'
30 | content += `PKG:Intel | Apple Silicon\n`
32 | content += '\nLinux:\n'
33 | content += `DEB:64位 | ARM64\n`
35 | content += `RPM:64位 | ARM64`
36 |
37 | await axios.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
38 | chat_id,
39 | text: content,
40 | link_preview_options: {
41 | is_disabled: false,
42 | url: 'https://github.com/mihomo-party-org/mihomo-party',
43 | prefer_large_media: true
44 | },
45 | parse_mode: 'HTML'
46 | })
47 |
--------------------------------------------------------------------------------
/scripts/updater.mjs:
--------------------------------------------------------------------------------
1 | import yaml from 'yaml'
2 | import { readFileSync, writeFileSync } from 'fs'
3 |
4 | const pkg = readFileSync('package.json', 'utf-8')
5 | let changelog = readFileSync('changelog.md', 'utf-8')
6 | const { version } = JSON.parse(pkg)
7 | const downloadUrl = `https://github.com/xishang0128/sparkle/releases/download/${version}`
8 | const latest = {
9 | version,
10 | changelog
11 | }
12 |
13 | if (!version.includes('beta')) {
14 | changelog += '\n### 下载地址:\n\n#### Windows10/11:\n\n'
15 | changelog += `- 安装版:[64位](${downloadUrl}/sparkle-windows-${version}-x64-setup.exe) | [ARM64](${downloadUrl}/sparkle-windows-${version}-arm64-setup.exe)\n\n`
16 | changelog += '\n#### macOS 11+:\n\n'
17 | changelog += `- PKG:[Intel](${downloadUrl}/sparkle-macos-${version}-x64.pkg) | [Apple Silicon](${downloadUrl}/sparkle-macos-${version}-arm64.pkg)\n\n`
18 | changelog += '\n#### Linux:\n\n'
19 | changelog += `- DEB:[64位](${downloadUrl}/sparkle-linux-${version}-amd64.deb) | [ARM64](${downloadUrl}/sparkle-linux-${version}-arm64.deb)\n\n`
20 | changelog += `- RPM:[64位](${downloadUrl}/sparkle-linux-${version}-x86_64.rpm) | [ARM64](${downloadUrl}/sparkle-linux-${version}-aarch64.rpm)`
21 | }
22 | writeFileSync('latest.yml', yaml.stringify(latest))
23 | writeFileSync('changelog.md', changelog)
24 |
--------------------------------------------------------------------------------
/src/main/config/app.ts:
--------------------------------------------------------------------------------
1 | import { readFile, writeFile } from 'fs/promises'
2 | import { appConfigPath } from '../utils/dirs'
3 | import yaml from 'yaml'
4 | import { deepMerge } from '../utils/merge'
5 | import { defaultConfig } from '../utils/template'
6 |
7 | let appConfig: IAppConfig // config.yaml
8 |
9 | export async function getAppConfig(force = false): Promise {
10 | if (force || !appConfig) {
11 | const data = await readFile(appConfigPath(), 'utf-8')
12 | appConfig = yaml.parse(data, { merge: true }) || defaultConfig
13 | }
14 | if (typeof appConfig !== 'object') appConfig = defaultConfig
15 | return appConfig
16 | }
17 |
18 | export async function patchAppConfig(patch: Partial): Promise {
19 | appConfig = deepMerge(appConfig, patch)
20 | await writeFile(appConfigPath(), yaml.stringify(appConfig))
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/config/controledMihomo.ts:
--------------------------------------------------------------------------------
1 | import { controledMihomoConfigPath } from '../utils/dirs'
2 | import { readFile, writeFile } from 'fs/promises'
3 | import yaml from 'yaml'
4 | import { generateProfile } from '../core/factory'
5 | import { getAppConfig } from './app'
6 | import { defaultControledMihomoConfig } from '../utils/template'
7 | import { deepMerge } from '../utils/merge'
8 |
9 | let controledMihomoConfig: Partial // mihomo.yaml
10 |
11 | export async function getControledMihomoConfig(force = false): Promise> {
12 | if (force || !controledMihomoConfig) {
13 | const data = await readFile(controledMihomoConfigPath(), 'utf-8')
14 | controledMihomoConfig = yaml.parse(data, { merge: true }) || defaultControledMihomoConfig
15 | }
16 | if (typeof controledMihomoConfig !== 'object')
17 | controledMihomoConfig = defaultControledMihomoConfig
18 | return controledMihomoConfig
19 | }
20 |
21 | export async function patchControledMihomoConfig(patch: Partial): Promise {
22 | const { controlDns = true, controlSniff = true } = await getAppConfig()
23 | if (!controlDns) {
24 | delete controledMihomoConfig.dns
25 | delete controledMihomoConfig.hosts
26 | } else {
27 | // 从不接管状态恢复
28 | if (controledMihomoConfig.dns?.ipv6 === undefined) {
29 | controledMihomoConfig.dns = defaultControledMihomoConfig.dns
30 | }
31 | }
32 | if (!controlSniff) {
33 | delete controledMihomoConfig.sniffer
34 | } else {
35 | // 从不接管状态恢复
36 | if (!controledMihomoConfig.sniffer) {
37 | controledMihomoConfig.sniffer = defaultControledMihomoConfig.sniffer
38 | }
39 | }
40 | if (patch.dns?.['nameserver-policy']) {
41 | controledMihomoConfig.dns = controledMihomoConfig.dns || {}
42 | controledMihomoConfig.dns['nameserver-policy'] = patch.dns['nameserver-policy']
43 | }
44 | if (patch.dns?.['use-hosts']) {
45 | controledMihomoConfig.hosts = patch.hosts
46 | }
47 | controledMihomoConfig = deepMerge(controledMihomoConfig, patch)
48 | if (process.platform === 'darwin' && controledMihomoConfig.tun) {
49 | controledMihomoConfig.tun.device = undefined
50 | }
51 | await generateProfile()
52 | await writeFile(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig), 'utf-8')
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/config/index.ts:
--------------------------------------------------------------------------------
1 | export { getAppConfig, patchAppConfig } from './app'
2 | export { getControledMihomoConfig, patchControledMihomoConfig } from './controledMihomo'
3 | export {
4 | getProfile,
5 | getCurrentProfileItem,
6 | getProfileItem,
7 | getProfileConfig,
8 | getFileStr,
9 | setFileStr,
10 | setProfileConfig,
11 | addProfileItem,
12 | removeProfileItem,
13 | createProfile,
14 | getProfileStr,
15 | getProfileParseStr,
16 | setProfileStr,
17 | changeCurrentProfile,
18 | updateProfileItem
19 | } from './profile'
20 | export {
21 | getOverrideConfig,
22 | setOverrideConfig,
23 | getOverrideItem,
24 | addOverrideItem,
25 | removeOverrideItem,
26 | createOverride,
27 | getOverride,
28 | setOverride,
29 | updateOverrideItem
30 | } from './override'
31 |
--------------------------------------------------------------------------------
/src/main/config/override.ts:
--------------------------------------------------------------------------------
1 | import { overrideConfigPath, overridePath } from '../utils/dirs'
2 | import { getControledMihomoConfig } from './controledMihomo'
3 | import { readFile, writeFile, rm } from 'fs/promises'
4 | import { existsSync } from 'fs'
5 | import axios from 'axios'
6 | import yaml from 'yaml'
7 |
8 | let overrideConfig: IOverrideConfig // override.yaml
9 |
10 | export async function getOverrideConfig(force = false): Promise {
11 | if (force || !overrideConfig) {
12 | const data = await readFile(overrideConfigPath(), 'utf-8')
13 | overrideConfig = yaml.parse(data, { merge: true }) || { items: [] }
14 | }
15 | if (typeof overrideConfig !== 'object') overrideConfig = { items: [] }
16 | return overrideConfig
17 | }
18 |
19 | export async function setOverrideConfig(config: IOverrideConfig): Promise {
20 | overrideConfig = config
21 | await writeFile(overrideConfigPath(), yaml.stringify(overrideConfig), 'utf-8')
22 | }
23 |
24 | export async function getOverrideItem(id: string | undefined): Promise {
25 | const { items } = await getOverrideConfig()
26 | return items.find((item) => item.id === id)
27 | }
28 |
29 | export async function updateOverrideItem(item: IOverrideItem): Promise {
30 | const config = await getOverrideConfig()
31 | const index = config.items.findIndex((i) => i.id === item.id)
32 | if (index === -1) {
33 | throw new Error('Override not found')
34 | }
35 | config.items[index] = item
36 | await setOverrideConfig(config)
37 | }
38 |
39 | export async function addOverrideItem(item: Partial): Promise {
40 | const config = await getOverrideConfig()
41 | const newItem = await createOverride(item)
42 | if (await getOverrideItem(item.id)) {
43 | updateOverrideItem(newItem)
44 | } else {
45 | config.items.push(newItem)
46 | }
47 | await setOverrideConfig(config)
48 | }
49 |
50 | export async function removeOverrideItem(id: string): Promise {
51 | const config = await getOverrideConfig()
52 | const item = await getOverrideItem(id)
53 | config.items = config.items?.filter((item) => item.id !== id)
54 | await setOverrideConfig(config)
55 | await rm(overridePath(id, item?.ext || 'js'))
56 | }
57 |
58 | export async function createOverride(item: Partial): Promise {
59 | const id = item.id || new Date().getTime().toString(16)
60 | const newItem = {
61 | id,
62 | name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
63 | type: item.type,
64 | ext: item.ext || 'js',
65 | url: item.url,
66 | global: item.global || false,
67 | updated: new Date().getTime()
68 | } as IOverrideItem
69 | switch (newItem.type) {
70 | case 'remote': {
71 | const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
72 | if (!item.url) throw new Error('Empty URL')
73 | const res = await axios.get(item.url, {
74 | ...(mixedPort != 0 && {
75 | proxy: {
76 | protocol: 'http',
77 | host: '127.0.0.1',
78 | port: mixedPort
79 | }
80 | }),
81 | responseType: 'text'
82 | })
83 | const data = res.data
84 | await setOverride(id, newItem.ext, data)
85 | break
86 | }
87 | case 'local': {
88 | const data = item.file || ''
89 | setOverride(id, newItem.ext, data)
90 | break
91 | }
92 | }
93 |
94 | return newItem
95 | }
96 |
97 | export async function getOverride(id: string, ext: 'js' | 'yaml' | 'log'): Promise {
98 | if (!existsSync(overridePath(id, ext))) {
99 | return ''
100 | }
101 | return await readFile(overridePath(id, ext), 'utf-8')
102 | }
103 |
104 | export async function setOverride(id: string, ext: 'js' | 'yaml', content: string): Promise {
105 | await writeFile(overridePath(id, ext), content, 'utf-8')
106 | }
107 |
--------------------------------------------------------------------------------
/src/main/core/profileUpdater.ts:
--------------------------------------------------------------------------------
1 | import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
2 |
3 | const intervalPool: Record = {}
4 |
5 | export async function initProfileUpdater(): Promise {
6 | const { items, current } = await getProfileConfig()
7 | const currentItem = await getCurrentProfileItem()
8 | for (const item of items.filter((i) => i.id !== current)) {
9 | if (item.type === 'remote' && item.interval) {
10 | intervalPool[item.id] = setTimeout(
11 | async () => {
12 | try {
13 | await addProfileItem(item)
14 | } catch (e) {
15 | /* ignore */
16 | }
17 | },
18 | item.interval * 60 * 1000
19 | )
20 | try {
21 | await addProfileItem(item)
22 | } catch (e) {
23 | /* ignore */
24 | }
25 | }
26 | }
27 | if (currentItem?.type === 'remote' && currentItem.interval) {
28 | intervalPool[currentItem.id] = setTimeout(
29 | async () => {
30 | try {
31 | await addProfileItem(currentItem)
32 | } catch (e) {
33 | /* ignore */
34 | }
35 | },
36 | currentItem.interval * 60 * 1000 + 10000 // +10s
37 | )
38 | try {
39 | await addProfileItem(currentItem)
40 | } catch (e) {
41 | /* ignore */
42 | }
43 | }
44 | }
45 |
46 | export async function addProfileUpdater(item: IProfileItem): Promise {
47 | if (item.type === 'remote' && item.interval) {
48 | if (intervalPool[item.id]) {
49 | clearTimeout(intervalPool[item.id])
50 | }
51 | intervalPool[item.id] = setTimeout(
52 | async () => {
53 | try {
54 | await addProfileItem(item)
55 | } catch (e) {
56 | /* ignore */
57 | }
58 | },
59 | item.interval * 60 * 1000
60 | )
61 | }
62 | }
63 |
64 | export async function delProfileUpdater(id: string): Promise {
65 | if (intervalPool[id]) {
66 | clearTimeout(intervalPool[id])
67 | delete intervalPool[id]
68 | }
69 | }
--------------------------------------------------------------------------------
/src/main/core/subStoreApi.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { subStorePort } from '../resolve/server'
3 | import { getAppConfig } from '../config'
4 |
5 | export async function subStoreSubs(): Promise {
6 | const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
7 | const baseUrl = useCustomSubStore ? customSubStoreUrl : `http://127.0.0.1:${subStorePort}`
8 | const res = await axios.get(`${baseUrl}/api/subs`, { responseType: 'json' })
9 | return res.data.data as ISubStoreSub[]
10 | }
11 |
12 | export async function subStoreCollections(): Promise {
13 | const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
14 | const baseUrl = useCustomSubStore ? customSubStoreUrl : `http://127.0.0.1:${subStorePort}`
15 | const res = await axios.get(`${baseUrl}/api/collections`, { responseType: 'json' })
16 | return res.data.data as ISubStoreSub[]
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/resolve/backup.ts:
--------------------------------------------------------------------------------
1 | import { getAppConfig } from '../config'
2 | import dayjs from 'dayjs'
3 | import AdmZip from 'adm-zip'
4 | import {
5 | appConfigPath,
6 | controledMihomoConfigPath,
7 | dataDir,
8 | overrideConfigPath,
9 | overrideDir,
10 | profileConfigPath,
11 | profilesDir,
12 | subStoreDir,
13 | themesDir
14 | } from '../utils/dirs'
15 |
16 | export async function webdavBackup(): Promise {
17 | const { createClient } = await import('webdav/dist/node/index.js')
18 | const {
19 | webdavUrl = '',
20 | webdavUsername = '',
21 | webdavPassword = '',
22 | webdavDir = 'sparkle'
23 | } = await getAppConfig()
24 | const zip = new AdmZip()
25 |
26 | zip.addLocalFile(appConfigPath())
27 | zip.addLocalFile(controledMihomoConfigPath())
28 | zip.addLocalFile(profileConfigPath())
29 | zip.addLocalFile(overrideConfigPath())
30 | zip.addLocalFolder(themesDir(), 'themes')
31 | zip.addLocalFolder(profilesDir(), 'profiles')
32 | zip.addLocalFolder(overrideDir(), 'override')
33 | zip.addLocalFolder(subStoreDir(), 'substore')
34 | const date = new Date()
35 | const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
36 |
37 | const client = createClient(webdavUrl, {
38 | username: webdavUsername,
39 | password: webdavPassword
40 | })
41 | try {
42 | await client.createDirectory(webdavDir)
43 | } catch {
44 | // ignore
45 | }
46 |
47 | return await client.putFileContents(`${webdavDir}/${zipFileName}`, zip.toBuffer())
48 | }
49 |
50 | export async function webdavRestore(filename: string): Promise {
51 | const { createClient } = await import('webdav/dist/node/index.js')
52 | const {
53 | webdavUrl = '',
54 | webdavUsername = '',
55 | webdavPassword = '',
56 | webdavDir = 'sparkle'
57 | } = await getAppConfig()
58 |
59 | const client = createClient(webdavUrl, {
60 | username: webdavUsername,
61 | password: webdavPassword
62 | })
63 | const zipData = await client.getFileContents(`${webdavDir}/${filename}`)
64 | const zip = new AdmZip(zipData as Buffer)
65 | zip.extractAllTo(dataDir(), true)
66 | }
67 |
68 | export async function listWebdavBackups(): Promise {
69 | const { createClient } = await import('webdav/dist/node/index.js')
70 | const {
71 | webdavUrl = '',
72 | webdavUsername = '',
73 | webdavPassword = '',
74 | webdavDir = 'sparkle'
75 | } = await getAppConfig()
76 |
77 | const client = createClient(webdavUrl, {
78 | username: webdavUsername,
79 | password: webdavPassword
80 | })
81 | const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
82 | if (Array.isArray(files)) {
83 | return files.map((file) => file.basename)
84 | } else {
85 | return files.data.map((file) => file.basename)
86 | }
87 | }
88 |
89 | export async function webdavDelete(filename: string): Promise {
90 | const { createClient } = await import('webdav/dist/node/index.js')
91 | const {
92 | webdavUrl = '',
93 | webdavUsername = '',
94 | webdavPassword = '',
95 | webdavDir = 'sparkle'
96 | } = await getAppConfig()
97 |
98 | const client = createClient(webdavUrl, {
99 | username: webdavUsername,
100 | password: webdavPassword
101 | })
102 | await client.deleteFile(`${webdavDir}/${filename}`)
103 | }
104 |
--------------------------------------------------------------------------------
/src/main/resolve/floatingWindow.ts:
--------------------------------------------------------------------------------
1 | import { is } from '@electron-toolkit/utils'
2 | import { BrowserWindow, ipcMain } from 'electron'
3 | import windowStateKeeper from 'electron-window-state'
4 | import { join } from 'path'
5 | import { getAppConfig, patchAppConfig } from '../config'
6 | import { applyTheme } from './theme'
7 | import { buildContextMenu, showTrayIcon } from './tray'
8 |
9 | export let floatingWindow: BrowserWindow | null = null
10 |
11 | async function preallocateGpuResources(): Promise {
12 | const preallocWin = new BrowserWindow({
13 | width: 1,
14 | height: 1,
15 | show: false,
16 | frame: false,
17 | webPreferences: {
18 | offscreen: true,
19 | sandbox: true
20 | }
21 | })
22 | await preallocWin.loadURL('about:blank')
23 | return new Promise((resolve) => {
24 | setTimeout(() => {
25 | if (!preallocWin.isDestroyed()) preallocWin.destroy()
26 | resolve()
27 | }, 300)
28 | })
29 | }
30 |
31 | async function createFloatingWindow(): Promise {
32 | // 预分配 GPU 资源,防止在创建悬浮窗时卡死
33 | await preallocateGpuResources()
34 |
35 | const floatingWindowState = windowStateKeeper({
36 | file: 'floating-window-state.json'
37 | })
38 | const { customTheme = 'default.css' } = await getAppConfig()
39 | floatingWindow = new BrowserWindow({
40 | width: 120,
41 | height: 42,
42 | x: floatingWindowState.x,
43 | y: floatingWindowState.y,
44 | show: false,
45 | frame: false,
46 | alwaysOnTop: true,
47 | resizable: false,
48 | transparent: true,
49 | skipTaskbar: true,
50 | minimizable: false,
51 | maximizable: false,
52 | fullscreenable: false,
53 | closable: false,
54 | webPreferences: {
55 | preload: join(__dirname, '../preload/index.js'),
56 | spellcheck: false,
57 | sandbox: false
58 | }
59 | })
60 | floatingWindowState.manage(floatingWindow)
61 | floatingWindow.on('ready-to-show', () => {
62 | applyTheme(customTheme)
63 | floatingWindow?.show()
64 | floatingWindow?.setAlwaysOnTop(true, 'screen-saver')
65 | })
66 | floatingWindow.on('moved', () => {
67 | if (floatingWindow) floatingWindowState.saveState(floatingWindow)
68 | })
69 | ipcMain.on('updateFloatingWindow', () => {
70 | if (floatingWindow) {
71 | floatingWindow?.webContents.send('controledMihomoConfigUpdated')
72 | floatingWindow?.webContents.send('appConfigUpdated')
73 | }
74 | })
75 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
76 | floatingWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/floating.html`)
77 | } else {
78 | floatingWindow.loadFile(join(__dirname, '../renderer/floating.html'))
79 | }
80 | }
81 |
82 | export async function showFloatingWindow(): Promise {
83 | if (floatingWindow) {
84 | floatingWindow.show()
85 | } else {
86 | await createFloatingWindow()
87 | }
88 | }
89 |
90 | export async function triggerFloatingWindow(): Promise {
91 | if (floatingWindow?.isVisible()) {
92 | await patchAppConfig({ showFloatingWindow: false })
93 | await closeFloatingWindow()
94 | } else {
95 | await patchAppConfig({ showFloatingWindow: true })
96 | await showFloatingWindow()
97 | }
98 | }
99 |
100 | export async function closeFloatingWindow(): Promise {
101 | if (floatingWindow) {
102 | floatingWindow.close()
103 | floatingWindow.destroy()
104 | floatingWindow = null
105 | }
106 | await showTrayIcon()
107 | await patchAppConfig({ disableTray: false })
108 | }
109 |
110 | export async function showContextMenu(): Promise {
111 | const menu = await buildContextMenu()
112 | menu.popup()
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/resolve/gistApi.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { getAppConfig, getControledMihomoConfig } from '../config'
3 | import { getRuntimeConfigStr } from '../core/factory'
4 |
5 | interface GistInfo {
6 | id: string
7 | description: string
8 | html_url: string
9 | }
10 |
11 | async function listGists(token: string): Promise {
12 | const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
13 | const res = await axios.get('https://api.github.com/gists', {
14 | headers: {
15 | Accept: 'application/vnd.github+json',
16 | Authorization: `Bearer ${token}`,
17 | 'X-GitHub-Api-Version': '2022-11-28'
18 | },
19 | ...(port != 0 && {
20 | proxy: {
21 | protocol: 'http',
22 | host: '127.0.0.1',
23 | port
24 | }
25 | }),
26 | responseType: 'json'
27 | })
28 | return res.data as GistInfo[]
29 | }
30 |
31 | async function createGist(token: string, content: string): Promise {
32 | const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
33 | return await axios.post(
34 | 'https://api.github.com/gists',
35 | {
36 | description: 'Auto Synced Sparkle Runtime Config',
37 | public: false,
38 | files: { 'sparkle.yaml': { content } }
39 | },
40 | {
41 | headers: {
42 | Accept: 'application/vnd.github+json',
43 | Authorization: `Bearer ${token}`,
44 | 'X-GitHub-Api-Version': '2022-11-28'
45 | },
46 | ...(port != 0 && {
47 | proxy: {
48 | protocol: 'http',
49 | host: '127.0.0.1',
50 | port
51 | }
52 | })
53 | }
54 | )
55 | }
56 |
57 | async function updateGist(token: string, id: string, content: string): Promise {
58 | const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
59 | return await axios.patch(
60 | `https://api.github.com/gists/${id}`,
61 | {
62 | description: 'Auto Synced Sparkle Runtime Config',
63 | files: { 'sparkle.yaml': { content } }
64 | },
65 | {
66 | headers: {
67 | Accept: 'application/vnd.github+json',
68 | Authorization: `Bearer ${token}`,
69 | 'X-GitHub-Api-Version': '2022-11-28'
70 | },
71 | ...(port != 0 && {
72 | proxy: {
73 | protocol: 'http',
74 | host: '127.0.0.1',
75 | port
76 | }
77 | })
78 | }
79 | )
80 | }
81 |
82 | export async function getGistUrl(): Promise {
83 | const { githubToken } = await getAppConfig()
84 | if (!githubToken) return ''
85 | const gists = await listGists(githubToken)
86 | const gist = gists.find((gist) => gist.description === 'Auto Synced Sparkle Runtime Config')
87 | if (gist) {
88 | return gist.html_url
89 | } else {
90 | await uploadRuntimeConfig()
91 | const gists = await listGists(githubToken)
92 | const gist = gists.find((gist) => gist.description === 'Auto Synced Sparkle Runtime Config')
93 | if (!gist) throw new Error('Gist not found')
94 | return gist.html_url
95 | }
96 | }
97 |
98 | export async function uploadRuntimeConfig(): Promise {
99 | const { githubToken } = await getAppConfig()
100 | if (!githubToken) return
101 | const gists = await listGists(githubToken)
102 | const gist = gists.find((gist) => gist.description === 'Auto Synced Sparkle Runtime Config')
103 | const config = await getRuntimeConfigStr()
104 | if (gist) {
105 | await updateGist(githubToken, gist.id, config)
106 | } else {
107 | await createGist(githubToken, config)
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/main/resolve/theme.ts:
--------------------------------------------------------------------------------
1 | import { copyFile, readdir, readFile, writeFile } from 'fs/promises'
2 | import { themesDir } from '../utils/dirs'
3 | import path from 'path'
4 | import axios from 'axios'
5 | import AdmZip from 'adm-zip'
6 | import { getControledMihomoConfig } from '../config'
7 | import { existsSync } from 'fs'
8 | import { mainWindow } from '..'
9 | import { floatingWindow } from './floatingWindow'
10 |
11 | let insertedCSSKeyMain: string | undefined = undefined
12 | let insertedCSSKeyFloating: string | undefined = undefined
13 |
14 | export async function resolveThemes(): Promise<{ key: string; label: string }[]> {
15 | const files = await readdir(themesDir())
16 | const themes = await Promise.all(
17 | files
18 | .filter((file) => file.endsWith('.css'))
19 | .map(async (file) => {
20 | const css = (await readFile(path.join(themesDir(), file), 'utf-8')) || ''
21 | let name = file
22 | if (css.startsWith('/*')) {
23 | name = css.split('\n')[0].replace('/*', '').replace('*/', '').trim() || file
24 | }
25 | return { key: file, label: name }
26 | })
27 | )
28 | if (themes.find((theme) => theme.key === 'default.css')) {
29 | return themes
30 | } else {
31 | return [{ key: 'default.css', label: '默认' }, ...themes]
32 | }
33 | }
34 |
35 | export async function fetchThemes(): Promise {
36 | const zipUrl = 'https://github.com/mihomo-party-org/theme-hub/releases/download/latest/themes.zip'
37 | const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
38 | const zipData = await axios.get(zipUrl, {
39 | responseType: 'arraybuffer',
40 | headers: { 'Content-Type': 'application/octet-stream' },
41 | ...(mixedPort != 0 && {
42 | proxy: {
43 | protocol: 'http',
44 | host: '127.0.0.1',
45 | port: mixedPort
46 | }
47 | })
48 | })
49 | const zip = new AdmZip(zipData.data as Buffer)
50 | zip.extractAllTo(themesDir(), true)
51 | }
52 |
53 | export async function importThemes(files: string[]): Promise {
54 | for (const file of files) {
55 | if (existsSync(file))
56 | await copyFile(
57 | file,
58 | path.join(themesDir(), `${new Date().getTime().toString(16)}-${path.basename(file)}`)
59 | )
60 | }
61 | }
62 |
63 | export async function readTheme(theme: string): Promise {
64 | if (!existsSync(path.join(themesDir(), theme))) return ''
65 | return await readFile(path.join(themesDir(), theme), 'utf-8')
66 | }
67 |
68 | export async function writeTheme(theme: string, css: string): Promise {
69 | await writeFile(path.join(themesDir(), theme), css)
70 | }
71 |
72 | export async function applyTheme(theme: string): Promise {
73 | const css = await readTheme(theme)
74 | await mainWindow?.webContents.removeInsertedCSS(insertedCSSKeyMain || '')
75 | insertedCSSKeyMain = await mainWindow?.webContents.insertCSS(css)
76 | try {
77 | await floatingWindow?.webContents.removeInsertedCSS(insertedCSSKeyFloating || '')
78 | insertedCSSKeyFloating = await floatingWindow?.webContents.insertCSS(css)
79 | } catch {
80 | // ignore
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/resolve/trafficMonitor.ts:
--------------------------------------------------------------------------------
1 | import { ChildProcess, spawn } from 'child_process'
2 | import { getAppConfig } from '../config'
3 | import { dataDir, resourcesFilesDir } from '../utils/dirs'
4 | import path from 'path'
5 | import { existsSync } from 'fs'
6 | import { readFile, rm, writeFile } from 'fs/promises'
7 |
8 | let child: ChildProcess
9 |
10 | export async function startMonitor(detached = false): Promise {
11 | if (process.platform !== 'win32') return
12 | if (existsSync(path.join(dataDir(), 'monitor.pid'))) {
13 | const pid = parseInt(await readFile(path.join(dataDir(), 'monitor.pid'), 'utf-8'))
14 | try {
15 | process.kill(pid, 'SIGINT')
16 | } catch {
17 | // ignore
18 | } finally {
19 | await rm(path.join(dataDir(), 'monitor.pid'))
20 | }
21 | }
22 | await stopMonitor()
23 | const { showTraffic = false } = await getAppConfig()
24 | if (!showTraffic) return
25 | child = spawn(path.join(resourcesFilesDir(), 'TrafficMonitor/TrafficMonitor.exe'), [], {
26 | cwd: path.join(resourcesFilesDir(), 'TrafficMonitor'),
27 | detached: detached,
28 | stdio: detached ? 'ignore' : undefined
29 | })
30 | if (detached) {
31 | if (child && child.pid) {
32 | await writeFile(path.join(dataDir(), 'monitor.pid'), child.pid.toString())
33 | }
34 | child.unref()
35 | }
36 | }
37 |
38 | async function stopMonitor(): Promise {
39 | if (child) {
40 | child.kill('SIGINT')
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/sys/interface.ts:
--------------------------------------------------------------------------------
1 | import os from 'os'
2 |
3 | export function getInterfaces(): NodeJS.Dict {
4 | return os.networkInterfaces()
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/sys/ssid.ts:
--------------------------------------------------------------------------------
1 | import { exec } from 'child_process'
2 | import { promisify } from 'util'
3 | import { getAppConfig, patchControledMihomoConfig } from '../config'
4 | import { patchMihomoConfig } from '../core/mihomoApi'
5 | import { mainWindow } from '..'
6 | import { ipcMain, net } from 'electron'
7 | import { getDefaultDevice } from '../core/manager'
8 |
9 | export async function getCurrentSSID(): Promise {
10 | if (process.platform === 'win32') {
11 | try {
12 | return await getSSIDByNetsh()
13 | } catch {
14 | return undefined
15 | }
16 | }
17 | if (process.platform === 'linux') {
18 | try {
19 | return await getSSIDByIwconfig()
20 | } catch {
21 | return undefined
22 | }
23 | }
24 | if (process.platform === 'darwin') {
25 | try {
26 | return await getSSIDByAirport()
27 | } catch {
28 | return await getSSIDByNetworksetup()
29 | }
30 | }
31 | return undefined
32 | }
33 |
34 | let lastSSID: string | undefined
35 | export async function checkSSID(): Promise {
36 | try {
37 | const { pauseSSID = [] } = await getAppConfig()
38 | if (pauseSSID.length === 0) return
39 | const currentSSID = await getCurrentSSID()
40 | if (currentSSID === lastSSID) return
41 | lastSSID = currentSSID
42 | if (currentSSID && pauseSSID.includes(currentSSID)) {
43 | await patchControledMihomoConfig({ mode: 'direct' })
44 | await patchMihomoConfig({ mode: 'direct' })
45 | mainWindow?.webContents.send('controledMihomoConfigUpdated')
46 | ipcMain.emit('updateTrayMenu')
47 | } else {
48 | await patchControledMihomoConfig({ mode: 'rule' })
49 | await patchMihomoConfig({ mode: 'rule' })
50 | mainWindow?.webContents.send('controledMihomoConfigUpdated')
51 | ipcMain.emit('updateTrayMenu')
52 | }
53 | } catch {
54 | // ignore
55 | }
56 | }
57 |
58 | export async function startSSIDCheck(): Promise {
59 | await checkSSID()
60 | setInterval(checkSSID, 30000)
61 | }
62 |
63 | async function getSSIDByAirport(): Promise {
64 | const execPromise = promisify(exec)
65 | const { stdout } = await execPromise(
66 | '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I'
67 | )
68 | if (stdout.trim().startsWith('WARNING')) {
69 | throw new Error('airport cannot be used')
70 | }
71 | for (const line of stdout.split('\n')) {
72 | if (line.trim().startsWith('SSID')) {
73 | return line.split(': ')[1].trim()
74 | }
75 | }
76 | return undefined
77 | }
78 |
79 | async function getSSIDByNetworksetup(): Promise {
80 | const execPromise = promisify(exec)
81 | if (net.isOnline()) {
82 | const service = await getDefaultDevice()
83 | const { stdout } = await execPromise(`networksetup -listpreferredwirelessnetworks ${service}`)
84 | if (stdout.trim().startsWith('Preferred networks on')) {
85 | if (stdout.split('\n').length > 1) {
86 | return stdout.split('\n')[1].trim()
87 | }
88 | }
89 | }
90 | return undefined
91 | }
92 |
93 | async function getSSIDByNetsh(): Promise {
94 | const execPromise = promisify(exec)
95 | const { stdout } = await execPromise('netsh wlan show interfaces')
96 | for (const line of stdout.split('\n')) {
97 | if (line.trim().startsWith('SSID')) {
98 | return line.split(': ')[1].trim()
99 | }
100 | }
101 | return undefined
102 | }
103 |
104 | async function getSSIDByIwconfig(): Promise {
105 | const execPromise = promisify(exec)
106 | const { stdout } = await execPromise(
107 | `iwconfig 2>/dev/null | grep 'ESSID' | awk -F'"' '{print $2}'`
108 | )
109 | if (stdout.trim() !== '') {
110 | return stdout.trim()
111 | }
112 | return undefined
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/utils/calc.ts:
--------------------------------------------------------------------------------
1 | export function calcTraffic(byte: number): string {
2 | if (byte < 1024) return `${byte} B`
3 | byte /= 1024
4 | if (byte < 1024) return `${formatNumString(byte)} KB`
5 | byte /= 1024
6 | if (byte < 1024) return `${formatNumString(byte)} MB`
7 | byte /= 1024
8 | if (byte < 1024) return `${formatNumString(byte)} GB`
9 | byte /= 1024
10 | if (byte < 1024) return `${formatNumString(byte)} TB`
11 | byte /= 1024
12 | if (byte < 1024) return `${formatNumString(byte)} PB`
13 | byte /= 1024
14 | if (byte < 1024) return `${formatNumString(byte)} EB`
15 | byte /= 1024
16 | if (byte < 1024) return `${formatNumString(byte)} ZB`
17 | byte /= 1024
18 | return `${formatNumString(byte)} YB`
19 | }
20 |
21 | function formatNumString(num: number): string {
22 | let str = num.toFixed(2)
23 | if (str.length <= 5) return str
24 | if (str.length == 6) {
25 | str = num.toFixed(1)
26 | return str
27 | } else {
28 | str = Math.round(num).toString()
29 | return str
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/utils/image.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { getControledMihomoConfig } from '../config'
3 |
4 | export async function getImageDataURL(url: string): Promise {
5 | const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
6 | const res = await axios.get(url, {
7 | responseType: 'arraybuffer',
8 | ...(port != 0 && {
9 | proxy: {
10 | protocol: 'http',
11 | host: '127.0.0.1',
12 | port
13 | }
14 | })
15 | })
16 | const mimeType = res.headers['content-type']
17 | const dataURL = `data:${mimeType};base64,${Buffer.from(res.data).toString('base64')}`
18 | return dataURL
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/utils/merge.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
2 | function isObject(item: any): boolean {
3 | return item && typeof item === 'object' && !Array.isArray(item)
4 | }
5 |
6 | function trimWrap(str: string): string {
7 | if (str.startsWith('<') && str.endsWith('>')) {
8 | return str.slice(1, -1)
9 | }
10 | return str
11 | }
12 |
13 | export function deepMerge(target: T, other: Partial, isOverride?: boolean): T {
14 | for (const key in other) {
15 | if (isObject(other[key])) {
16 | if (key.endsWith('!')) {
17 | const k = trimWrap(key.slice(0, -1))
18 | target[k] = other[key]
19 | } else {
20 | const k = trimWrap(key)
21 | if (!target[k]) Object.assign(target, { [k]: {} })
22 | deepMerge(target[k] as object, other[k] as object, isOverride)
23 | }
24 | } else if (Array.isArray(other[key])) {
25 | if (isOverride && key.startsWith('+')) {
26 | const k = trimWrap(key.slice(1))
27 | if (!target[k]) Object.assign(target, { [k]: [] })
28 | target[k] = [...other[key], ...(target[k] as never[])]
29 | } else if (isOverride && key.endsWith('+')) {
30 | const k = trimWrap(key.slice(0, -1))
31 | if (!target[k]) Object.assign(target, { [k]: [] })
32 | target[k] = [...(target[k] as never[]), ...other[key]]
33 | } else {
34 | const k = trimWrap(key)
35 | Object.assign(target, { [k]: other[key] })
36 | }
37 | } else {
38 | Object.assign(target, { [key]: other[key] })
39 | }
40 | }
41 | return target as T
42 | }
43 |
--------------------------------------------------------------------------------
/src/preload/index.d.ts:
--------------------------------------------------------------------------------
1 | import { ElectronAPI } from '@electron-toolkit/preload'
2 | import { webUtils } from 'electron'
3 |
4 | declare global {
5 | interface Window {
6 | electron: ElectronAPI
7 | api: { webUtils: typeof webUtils }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/preload/index.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge, webUtils } from 'electron'
2 | import { electronAPI } from '@electron-toolkit/preload'
3 |
4 | // Custom APIs for renderer
5 | const api = {
6 | webUtils: webUtils
7 | }
8 | // Use `contextBridge` APIs to expose Electron APIs to
9 | // renderer only if context isolation is enabled, otherwise
10 | // just add to the DOM global.
11 | if (process.contextIsolated) {
12 | try {
13 | contextBridge.exposeInMainWorld('electron', electronAPI)
14 | contextBridge.exposeInMainWorld('api', api)
15 | } catch (error) {
16 | console.error(error)
17 | }
18 | } else {
19 | // @ts-ignore (define in dts)
20 | window.electron = electronAPI
21 | // @ts-ignore (define in dts)
22 | window.api = api
23 | }
24 |
--------------------------------------------------------------------------------
/src/renderer/floating.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Sparkle Floating
6 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/renderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Sparkle
6 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/renderer/src/FloatingApp.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useState } from 'react'
2 | import MihomoIcon from './components/base/mihomo-icon'
3 | import { calcTraffic } from './utils/calc'
4 | import { showContextMenu, triggerMainWindow } from './utils/ipc'
5 | import { useAppConfig } from './hooks/use-app-config'
6 | import { useControledMihomoConfig } from './hooks/use-controled-mihomo-config'
7 |
8 | const FloatingApp: React.FC = () => {
9 | const { appConfig } = useAppConfig()
10 | const { controledMihomoConfig } = useControledMihomoConfig()
11 | const { sysProxy, spinFloatingIcon = true } = appConfig || {}
12 | const { tun } = controledMihomoConfig || {}
13 | const sysProxyEnabled = sysProxy?.enable
14 | const tunEnabled = tun?.enable
15 |
16 | const [upload, setUpload] = useState(0)
17 | const [download, setDownload] = useState(0)
18 |
19 | // 根据总速率计算旋转速度
20 | const spinSpeed = useMemo(() => {
21 | const total = upload + download
22 | if (total === 0) return 0
23 | if (total < 1024) return 2
24 | if (total < 1024 * 1024) return 3
25 | if (total < 1024 * 1024 * 1024) return 4
26 | return 5
27 | }, [upload, download])
28 |
29 | const [rotation, setRotation] = useState(0)
30 |
31 | useEffect(() => {
32 | if (!spinFloatingIcon) return
33 |
34 | let animationFrameId: number
35 | const animate = (): void => {
36 | setRotation((prev) => {
37 | if (prev === 360) {
38 | return 0
39 | }
40 | return prev + spinSpeed
41 | })
42 | animationFrameId = requestAnimationFrame(animate)
43 | }
44 |
45 | animationFrameId = requestAnimationFrame(animate)
46 | return (): void => {
47 | cancelAnimationFrame(animationFrameId)
48 | }
49 | }, [spinSpeed, spinFloatingIcon])
50 |
51 | useEffect(() => {
52 | window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
53 | setUpload(info.up)
54 | setDownload(info.down)
55 | })
56 | return (): void => {
57 | window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
58 | }
59 | }, [])
60 |
61 | return (
62 |
63 |
64 |
65 |
{
67 | e.preventDefault()
68 | showContextMenu()
69 | }}
70 | onClick={() => {
71 | triggerMainWindow()
72 | }}
73 | style={
74 | spinFloatingIcon
75 | ? {
76 | transform: `rotate(${rotation}deg)`,
77 | transition: 'transform 0.1s linear'
78 | }
79 | : {}
80 | }
81 | className={`app-nodrag cursor-pointer floating-thumb ${tunEnabled ? 'bg-secondary' : sysProxyEnabled ? 'bg-primary' : 'bg-default'} hover:opacity-hover rounded-full h-[calc(100%-4px)] aspect-square`}
82 | >
83 |
84 |
85 |
86 |
87 |
88 |
89 | {calcTraffic(upload)}/s
90 |
91 |
92 | {calcTraffic(download)}/s
93 |
94 |
95 |
96 |
97 |
98 | )
99 | }
100 |
101 | export default FloatingApp
102 |
--------------------------------------------------------------------------------
/src/renderer/src/assets/floating.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .floating-text {
6 | font-family:
7 | 'Microsoft YaHei',
8 | system-ui,
9 | -apple-system,
10 | BlinkMacSystemFont;
11 | }
12 |
13 | html {
14 | background: none !important;
15 | background-color: transparent !important;
16 | }
17 |
18 | .app-nodrag {
19 | -webkit-app-region: none;
20 | }
21 |
22 | .app-drag {
23 | -webkit-app-region: drag;
24 | }
25 |
26 | * {
27 | user-select: none;
28 | }
29 |
--------------------------------------------------------------------------------
/src/renderer/src/assets/main.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @font-face {
6 | font-family: 'Noto Color Emoji';
7 | src: url('./NotoColorEmoji.ttf');
8 | }
9 |
10 | .driver-popover {
11 | background-color: hsl(var(--heroui-content2)) !important;
12 | border-radius: 8px !important;
13 | color: hsl(var(--heroui-foreground)) !important;
14 | }
15 |
16 | .driver-popover a {
17 | color: hsl(var(--heroui-primary)) !important;
18 | text-decoration: underline !important;
19 | }
20 |
21 | .driver-popover-close-btn {
22 | color: hsl(var(--heroui-foreground)) !important;
23 | }
24 |
25 | .driver-popover-progress-text {
26 | color: hsl(var(--heroui-default-500)) !important;
27 | }
28 |
29 | .driver-popover-prev-btn {
30 | color: white !important;
31 | text-shadow: none !important;
32 | border: none !important;
33 | padding: 8px !important;
34 | border-radius: 5px !important;
35 | font-size: 12px !important;
36 | background-color: hsl(var(--heroui-primary)) !important;
37 | }
38 |
39 | .driver-popover-next-btn {
40 | color: white !important;
41 | text-shadow: none !important;
42 | border: none !important;
43 | padding: 8px !important;
44 | border-radius: 5px !important;
45 | font-size: 12px !important;
46 | background-color: hsl(var(--heroui-primary)) !important;
47 | }
48 |
49 | .driver-popover-arrow-side-bottom {
50 | border-bottom-color: hsl(var(--heroui-content2)) !important;
51 | }
52 |
53 | .driver-popover-arrow-side-top {
54 | border-top-color: hsl(var(--heroui-content2)) !important;
55 | }
56 |
57 | .driver-popover-arrow-side-left {
58 | border-left-color: hsl(var(--heroui-content2)) !important;
59 | }
60 |
61 | .driver-popover-arrow-side-right {
62 | border-right-color: hsl(var(--heroui-content2)) !important;
63 | }
64 |
65 | .app-nodrag {
66 | -webkit-app-region: none;
67 | }
68 | .app-drag {
69 | -webkit-app-region: drag;
70 | }
71 |
72 | * {
73 | user-select: none;
74 | }
75 |
76 | *:focus {
77 | outline: none;
78 | }
79 |
80 | .flag-emoji {
81 | font-family:
82 | system-ui,
83 | -apple-system,
84 | BlinkMacSystemFont,
85 | 'Segoe UI',
86 | Roboto,
87 | Oxygen,
88 | Ubuntu,
89 | Cantarell,
90 | 'Open Sans',
91 | 'Helvetica Neue',
92 | sans-serif,
93 | 'Apple Color Emoji',
94 | 'Noto Color Emoji';
95 | }
96 |
97 | .no-scrollbar::-webkit-scrollbar {
98 | display: none;
99 | }
100 |
101 | *::-webkit-scrollbar {
102 | width: 8px;
103 | height: 8px;
104 | }
105 |
106 | *::-webkit-scrollbar-corner {
107 | background-color: transparent;
108 | }
109 |
110 | /* Light mode */
111 | @media (prefers-color-scheme: light) {
112 | *::-webkit-scrollbar-thumb {
113 | background: #c0c1c58f;
114 | border-radius: 5px;
115 | }
116 |
117 | *::-webkit-scrollbar-thumb:hover {
118 | background: #c0c1c550;
119 | }
120 | }
121 |
122 | /* Dark mode */
123 | @media (prefers-color-scheme: dark) {
124 | *::-webkit-scrollbar-thumb {
125 | background: #c0c1c550;
126 | border-radius: 5px;
127 | }
128 |
129 | *::-webkit-scrollbar-thumb:hover {
130 | background: #c0c1c58f;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/renderer/src/components/base/base-confirm.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
3 |
4 | interface ConfirmModalProps {
5 | onChange: (open: boolean) => void
6 | title?: string
7 | description?: React.ReactNode
8 | confirmText?: string
9 | cancelText?: string
10 | onConfirm: () => void | Promise
11 | }
12 |
13 | const ConfirmModal: React.FC = ({
14 | onChange,
15 | title = '请确认',
16 | description,
17 | confirmText = '确认',
18 | cancelText = '取消',
19 | onConfirm
20 | }) => (
21 |
33 |
34 | {title}
35 |
36 | {description}
37 |
38 |
39 |
42 |
52 |
53 |
54 |
55 | )
56 |
57 | export default ConfirmModal
58 |
--------------------------------------------------------------------------------
/src/renderer/src/components/base/base-error-boundary.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@heroui/react'
2 | import { JSX, ReactNode } from 'react'
3 | import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
4 |
5 | const ErrorFallback = ({ error }: FallbackProps): JSX.Element => {
6 | return (
7 |
8 |
9 | {'应用崩溃了 :( 请将以下信息提交给开发者以排查错误'}
10 |
11 |
12 | {/*
*/}
20 |
29 |
30 |
40 |
41 |
{error.message}
42 |
43 |
44 | Error Stack
45 | {error.stack}
46 |
47 |
48 | )
49 | }
50 |
51 | interface Props {
52 | children?: ReactNode
53 | }
54 |
55 | const BaseErrorBoundary = (props: Props): JSX.Element => {
56 | return {props.children}
57 | }
58 |
59 | export default BaseErrorBoundary
60 |
--------------------------------------------------------------------------------
/src/renderer/src/components/base/base-page.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Divider } from '@heroui/react'
2 | import { useAppConfig } from '@renderer/hooks/use-app-config'
3 | import { platform } from '@renderer/utils/init'
4 | import { isAlwaysOnTop, setAlwaysOnTop } from '@renderer/utils/ipc'
5 | import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
6 | import { RiPushpin2Fill, RiPushpin2Line } from 'react-icons/ri'
7 | interface Props {
8 | title?: React.ReactNode
9 | header?: React.ReactNode
10 | children?: React.ReactNode
11 | contentClassName?: string
12 | }
13 | let saveOnTop = false
14 |
15 | const BasePage = forwardRef((props, ref) => {
16 | const { appConfig } = useAppConfig()
17 | const { useWindowFrame = false } = appConfig || {}
18 | const [overlayWidth, setOverlayWidth] = React.useState(0)
19 | const [onTop, setOnTop] = useState(saveOnTop)
20 |
21 | const updateAlwaysOnTop = async (): Promise => {
22 | setOnTop(await isAlwaysOnTop())
23 | saveOnTop = await isAlwaysOnTop()
24 | }
25 |
26 | useEffect(() => {
27 | if (platform !== 'darwin' && !useWindowFrame) {
28 | try {
29 | // @ts-ignore windowControlsOverlay
30 | const windowControlsOverlay = window.navigator.windowControlsOverlay
31 | setOverlayWidth(window.innerWidth - windowControlsOverlay.getTitlebarAreaRect().width)
32 | } catch (e) {
33 | // ignore
34 | }
35 | }
36 | }, [])
37 |
38 | const contentRef = useRef(null)
39 | useImperativeHandle(ref, () => {
40 | return contentRef.current as HTMLDivElement
41 | })
42 |
43 | return (
44 |
45 |
46 |
47 |
{props.title}
48 |
49 | {props.header}
50 |
70 |
71 |
72 |
73 |
74 |
75 | {props.children}
76 |
77 |
78 | )
79 | })
80 |
81 | BasePage.displayName = 'BasePage'
82 | export default BasePage
83 |
--------------------------------------------------------------------------------
/src/renderer/src/components/base/base-setting-card.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Accordion, AccordionItem, Card, CardBody } from '@heroui/react'
3 |
4 | interface Props {
5 | title?: string
6 | children?: React.ReactNode
7 | className?: string
8 | }
9 |
10 | const SettingCard: React.FC = (props) => {
11 | return !props.title ? (
12 |
13 | {props.children}
14 |
15 | ) : (
16 |
17 |
23 | {props.children}
24 |
25 |
26 | )
27 | }
28 |
29 | export default SettingCard
30 |
--------------------------------------------------------------------------------
/src/renderer/src/components/base/base-setting-item.tsx:
--------------------------------------------------------------------------------
1 | import { Divider } from '@heroui/react'
2 |
3 | import React from 'react'
4 |
5 | interface Props {
6 | title: React.ReactNode
7 | actions?: React.ReactNode
8 | children?: React.ReactNode
9 | divider?: boolean
10 | }
11 |
12 | const SettingItem: React.FC = (props) => {
13 | const { title, actions, children, divider = false } = props
14 |
15 | return (
16 | <>
17 |
18 |
19 |
{title}
20 |
{actions}
21 |
22 | {children}
23 |
24 | {divider && }
25 | >
26 | )
27 | }
28 |
29 | export default SettingItem
30 |
--------------------------------------------------------------------------------
/src/renderer/src/components/base/border-switch.css:
--------------------------------------------------------------------------------
1 | .border-switch {
2 | input[type='checkbox'] {
3 | width: 100%;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/renderer/src/components/base/border-swtich.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { cn, Switch, SwitchProps } from '@heroui/react'
3 | import './border-switch.css'
4 |
5 | interface SiderSwitchProps extends SwitchProps {
6 | isShowBorder?: boolean
7 | }
8 |
9 | const BorderSwitch: React.FC = (props) => {
10 | const { isShowBorder = false, classNames, ...switchProps } = props
11 |
12 | return (
13 |
26 | )
27 | }
28 |
29 | export default BorderSwitch
30 |
--------------------------------------------------------------------------------
/src/renderer/src/components/base/collapse-input.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react'
2 | import { Input, InputProps } from '@heroui/react'
3 | import { FaSearch } from 'react-icons/fa'
4 |
5 | interface CollapseInputProps extends InputProps {
6 | title: string
7 | }
8 |
9 | const CollapseInput: React.FC = (props) => {
10 | const { title, ...inputProps } = props
11 | const inputRef = useRef(null)
12 | return (
13 |
14 | {
27 | e.stopPropagation()
28 | inputRef.current?.focus()
29 | }}
30 | >
31 |
32 |
33 | }
34 | onClick={(e) => {
35 | e.stopPropagation()
36 | inputRef.current?.focus()
37 | }}
38 | />
39 |
40 | )
41 | }
42 |
43 | export default CollapseInput
44 |
--------------------------------------------------------------------------------
/src/renderer/src/components/base/interface-select.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Select, SelectItem } from '@heroui/react'
3 | import { getInterfaces } from '@renderer/utils/ipc'
4 |
5 | const InterfaceSelect: React.FC<{
6 | value: string
7 | exclude?: string[]
8 | onChange: (iface: string) => void
9 | }> = ({ value, onChange, exclude = [] }) => {
10 | const [ifaces, setIfaces] = useState([])
11 | useEffect(() => {
12 | const fetchInterfaces = async () => {
13 | const names = Object.keys(await getInterfaces())
14 | setIfaces(names.filter((name) => !exclude.includes(name)))
15 | }
16 | fetchInterfaces()
17 | }, [])
18 |
19 | return (
20 |
34 | )
35 | }
36 |
37 | export default InterfaceSelect
38 |
--------------------------------------------------------------------------------
/src/renderer/src/components/base/mihomo-icon.tsx:
--------------------------------------------------------------------------------
1 | import { JSX } from 'react'
2 | import { GenIcon, IconBaseProps } from 'react-icons'
3 | function MihomoIcon(props: IconBaseProps): JSX.Element {
4 | return GenIcon({
5 | tag: 'svg',
6 | attr: { viewBox: '0 0 76.14 72.14' },
7 | child: [
8 | {
9 | tag: 'path',
10 | attr: {
11 | d: 'm38.2,35.17c-2.73,0-5.58.46-8.54,1.4-.36.12-.75.03-1.04-.23l-11.79-10.76c-.66-.6-1.69-.55-2.29.11-.27.3-.42.69-.42,1.09v25.51c0,.95-.48,1.42-1.43,1.42-3.49,0-7.45-.69-11.89-2.06-.27-.09-.46-.34-.46-.63V1.45c0-.48.24-.74.72-.78L9.19,0c.23-.02.46.06.63.21l22.99,20.97c.37.33.87.48,1.35.39,1.55-.28,2.9-.42,4.05-.42s2.5.14,4.05.43c.49.09.99-.05,1.36-.39L66.64.26c.17-.15.4-.23.63-.21l8.13.68c.48.04.72.3.72.78l-.09,49.57c0,.29-.19.54-.46.63-4.44,1.36-8.4,2.04-11.89,2.04-.95,0-1.43-.47-1.43-1.42l.05-25.51c0-.89-.72-1.62-1.62-1.62-.4,0-.79.15-1.09.42l-11.81,10.74c-.29.26-.68.34-1.04.22-2.96-.94-5.81-1.41-8.54-1.41Z'
12 | },
13 | child: []
14 | },
15 | {
16 | tag: 'path',
17 | attr: {
18 | d: 'm38.6,54.9c.94.02,1.42.03,1.43.03.73.04,1.3.66,1.27,1.38,0,.19-.06.37-.15.54-.57,1.07-1.11,2.04-1.64,2.91-.11.18-.25.3-.42.37s-.38.1-.61.09c-.23,0-.43-.04-.6-.12-.17-.07-.3-.2-.4-.38-.49-.89-1-1.88-1.52-2.98-.31-.65-.02-1.44.65-1.74.17-.08.36-.12.55-.13.01,0,.49.01,1.44.03Z'
19 | },
20 | child: []
21 | },
22 | {
23 | tag: 'rect',
24 | attr: {
25 | x: '0',
26 | y: '57',
27 | width: '21.12',
28 | height: '2.7',
29 | rx: '.9',
30 | ry: '.9'
31 | },
32 | child: []
33 | },
34 | {
35 | tag: 'rect',
36 | attr: {
37 | x: '55.81',
38 | y: '57',
39 | width: '20.28',
40 | height: '2.7',
41 | rx: '.9',
42 | ry: '.9'
43 | },
44 | child: []
45 | },
46 | {
47 | tag: 'rect',
48 | attr: {
49 | x: '-.29',
50 | y: '66.23',
51 | width: '21.96',
52 | height: '2.8',
53 | rx: '.9',
54 | ry: '.9',
55 | transform: 'translate(-21.33 7.11) rotate(-18.9)'
56 | },
57 | child: []
58 | },
59 | {
60 | tag: 'rect',
61 | attr: {
62 | x: '64.5',
63 | y: '56.89',
64 | width: '2.82',
65 | height: '21.4',
66 | rx: '.9',
67 | ry: '.9',
68 | transform: 'translate(-19.39 108.06) rotate(-71.1)'
69 | },
70 | child: []
71 | }
72 | ]
73 | })(props)
74 | }
75 |
76 | export default MihomoIcon
77 |
--------------------------------------------------------------------------------
/src/renderer/src/components/connections/connection-item.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardFooter, CardHeader, Chip } from '@heroui/react'
2 | import { calcTraffic } from '@renderer/utils/calc'
3 | import dayjs from 'dayjs'
4 | import React, { useEffect } from 'react'
5 | import { CgClose, CgTrash } from 'react-icons/cg'
6 |
7 | interface Props {
8 | index: number
9 | info: IMihomoConnectionDetail
10 | selected: IMihomoConnectionDetail | undefined
11 | setSelected: React.Dispatch>
12 | setIsDetailModalOpen: React.Dispatch>
13 | close: (id: string) => void
14 | }
15 |
16 | const ConnectionItem: React.FC = (props) => {
17 | const { index, info, close, selected, setSelected, setIsDetailModalOpen } = props
18 |
19 | useEffect(() => {
20 | if (selected?.id === info.id) {
21 | setSelected(info)
22 | }
23 | }, [info])
24 |
25 | return (
26 |
27 |
{
31 | setSelected(info)
32 | setIsDetailModalOpen(true)
33 | }}
34 | >
35 |
36 |
37 |
38 |
44 | {info.metadata.type}({info.metadata.network.toUpperCase()})
45 |
46 |
47 | {info.metadata.process || info.metadata.sourceIP}
48 | {' -> '}
49 | {info.metadata.host ||
50 | info.metadata.sniffHost ||
51 | info.metadata.destinationIP ||
52 | info.metadata.remoteDestination}
53 |
54 |
55 | {dayjs(info.start).fromNow()}
56 |
57 |
58 |
{
60 | e.currentTarget.scrollLeft += e.deltaY
61 | }}
62 | className="overscroll-contain pt-2 flex justify-start gap-1 overflow-x-auto no-scrollbar"
63 | >
64 |
70 | {info.chains[0]}
71 |
72 |
73 | ↑ {calcTraffic(info.upload)} ↓ {calcTraffic(info.download)}
74 |
75 | {info.uploadSpeed !== 0 || info.downloadSpeed !== 0 ? (
76 |
77 | ↑ {calcTraffic(info.uploadSpeed || 0)}/s ↓ {calcTraffic(info.downloadSpeed || 0)}
78 | /s
79 |
80 | ) : null}
81 |
82 |
83 |
94 |
95 |
96 |
97 | )
98 | }
99 |
100 | export default ConnectionItem
101 |
--------------------------------------------------------------------------------
/src/renderer/src/components/logs/log-item.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardBody, CardHeader } from '@heroui/react'
2 | import React from 'react'
3 |
4 | const colorMap = {
5 | error: 'danger',
6 | warning: 'warning',
7 | info: 'primary',
8 | debug: 'default'
9 | }
10 | const LogItem: React.FC = (props) => {
11 | const { type, payload, time, index } = props
12 | return (
13 |
14 |
15 |
16 |
17 | {props.type.toUpperCase()}
18 |
19 | {time}
20 |
21 | {payload}
22 |
23 |
24 | )
25 | }
26 |
27 | export default LogItem
28 |
--------------------------------------------------------------------------------
/src/renderer/src/components/mihomo/env-setting.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import SettingCard from '../base/base-setting-card'
3 | import SettingItem from '../base/base-setting-item'
4 | import { Button, Divider, Switch } from '@heroui/react'
5 | import { useAppConfig } from '@renderer/hooks/use-app-config'
6 | import { restartCore } from '@renderer/utils/ipc'
7 | import EditableList from '../base/base-list-editor'
8 | import { platform } from '@renderer/utils/init'
9 |
10 | const EnvSetting: React.FC = () => {
11 | const { appConfig, patchAppConfig } = useAppConfig()
12 | const {
13 | disableLoopbackDetector,
14 | disableEmbedCA,
15 | disableSystemCA,
16 | disableNftables,
17 | skipSafePathCheck,
18 | safePaths = []
19 | } = appConfig || {}
20 | const handleConfigChangeWithRestart = async (key: string, value: any) => {
21 | try {
22 | await patchAppConfig({ [key]: value })
23 | await restartCore()
24 | } catch (e) {
25 | alert(e)
26 | } finally {
27 | PubSub.publish('mihomo-core-changed')
28 | }
29 | }
30 | const [safePathsInput, setSafePathsInput] = useState(safePaths)
31 |
32 | return (
33 |
34 |
35 | {
39 | handleConfigChangeWithRestart('disableSystemCA', v)
40 | }}
41 | />
42 |
43 |
44 | {
48 | handleConfigChangeWithRestart('disableEmbedCA', v)
49 | }}
50 | />
51 |
52 |
53 | {
57 | handleConfigChangeWithRestart('disableLoopbackDetector', v)
58 | }}
59 | />
60 |
61 | {platform == 'linux' && (
62 |
63 | {
67 | handleConfigChangeWithRestart('disableNftables', v)
68 | }}
69 | />
70 |
71 | )}
72 |
73 | {
77 | handleConfigChangeWithRestart('skipSafePathCheck', v)
78 | }}
79 | />
80 |
81 | {!skipSafePathCheck && (
82 | <>
83 |
84 |
85 | {safePathsInput.join('') != safePaths.join('') && (
86 |
95 | )}
96 |
97 |
98 | >
99 | )}
100 |
101 | )
102 | }
103 |
104 | export default EnvSetting
105 |
--------------------------------------------------------------------------------
/src/renderer/src/components/mihomo/interface-modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | ModalContent,
4 | ModalHeader,
5 | ModalBody,
6 | ModalFooter,
7 | Button,
8 | Snippet
9 | } from '@heroui/react'
10 | import React, { useEffect, useState } from 'react'
11 | import { getInterfaces } from '@renderer/utils/ipc'
12 | interface Props {
13 | onClose: () => void
14 | }
15 | const InterfaceModal: React.FC = (props) => {
16 | const { onClose } = props
17 | const [info, setInfo] = useState>({})
18 | const getInfo = async (): Promise => {
19 | setInfo(await getInterfaces())
20 | }
21 |
22 | useEffect(() => {
23 | getInfo()
24 | }, [])
25 |
26 | return (
27 |
35 |
36 | 网络信息
37 |
38 | {Object.entries(info).map(([key, value]) => {
39 | return (
40 |
41 |
{key}
42 | {value.map((v) => {
43 | return (
44 |
45 |
46 | {v.family}
47 |
48 | {v.address}
49 |
50 |
51 |
52 | )
53 | })}
54 |
55 | )
56 | })}
57 |
58 |
59 |
62 |
63 |
64 |
65 | )
66 | }
67 |
68 | export default InterfaceModal
69 |
--------------------------------------------------------------------------------
/src/renderer/src/components/override/edit-file-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
2 | import React, { useEffect, useState } from 'react'
3 | import { BaseEditor } from '../base/base-editor'
4 | import { getOverride, restartCore, setOverride } from '@renderer/utils/ipc'
5 | interface Props {
6 | id: string
7 | language: 'javascript' | 'yaml'
8 | onClose: () => void
9 | }
10 | const EditFileModal: React.FC = (props) => {
11 | const { id, language, onClose } = props
12 | const [currData, setCurrData] = useState('')
13 |
14 | const getContent = async (): Promise => {
15 | setCurrData(await getOverride(id, language === 'javascript' ? 'js' : 'yaml'))
16 | }
17 |
18 | useEffect(() => {
19 | getContent()
20 | }, [])
21 |
22 | return (
23 |
35 |
36 |
37 | 编辑覆写{language === 'javascript' ? '脚本' : '配置'}
38 |
39 |
40 | setCurrData(value)}
44 | />
45 |
46 |
47 |
50 |
65 |
66 |
67 |
68 | )
69 | }
70 |
71 | export default EditFileModal
72 |
--------------------------------------------------------------------------------
/src/renderer/src/components/override/edit-info-modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | ModalContent,
4 | ModalHeader,
5 | ModalBody,
6 | ModalFooter,
7 | Button,
8 | Input,
9 | Switch
10 | } from '@heroui/react'
11 | import React, { useState } from 'react'
12 | import SettingItem from '../base/base-setting-item'
13 | import { restartCore } from '@renderer/utils/ipc'
14 | interface Props {
15 | item: IOverrideItem
16 | updateOverrideItem: (item: IOverrideItem) => Promise
17 | onClose: () => void
18 | }
19 | const EditInfoModal: React.FC = (props) => {
20 | const { item, updateOverrideItem, onClose } = props
21 | const [values, setValues] = useState(item)
22 |
23 | const onSave = async (): Promise => {
24 | await updateOverrideItem(values)
25 | await restartCore()
26 | onClose()
27 | }
28 |
29 | return (
30 |
38 |
39 | 编辑信息
40 |
41 |
42 | {
47 | setValues({ ...values, name: v })
48 | }}
49 | />
50 |
51 | {values.type === 'remote' && (
52 |
53 | {
58 | setValues({ ...values, url: v })
59 | }}
60 | />
61 |
62 | )}
63 |
64 | {
68 | setValues({ ...values, global: v })
69 | }}
70 | />
71 |
72 |
73 |
74 |
77 |
80 |
81 |
82 |
83 | )
84 | }
85 |
86 | export default EditInfoModal
87 |
--------------------------------------------------------------------------------
/src/renderer/src/components/override/exec-log-modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | ModalContent,
4 | ModalHeader,
5 | ModalBody,
6 | ModalFooter,
7 | Button,
8 | Divider
9 | } from '@heroui/react'
10 | import React, { useEffect, useState } from 'react'
11 | import { getOverride } from '@renderer/utils/ipc'
12 | interface Props {
13 | id: string
14 | onClose: () => void
15 | }
16 | const ExecLogModal: React.FC = (props) => {
17 | const { id, onClose } = props
18 | const [logs, setLogs] = useState([])
19 |
20 | const getLog = async (): Promise => {
21 | setLogs((await getOverride(id, 'log')).split('\n').filter(Boolean))
22 | }
23 |
24 | useEffect(() => {
25 | getLog()
26 | }, [])
27 |
28 | return (
29 |
37 |
38 | 执行日志
39 |
40 | {logs.map((log) => {
41 | return (
42 | <>
43 | {log}
44 |
45 | >
46 | )
47 | })}
48 |
49 |
50 |
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default ExecLogModal
60 |
--------------------------------------------------------------------------------
/src/renderer/src/components/profiles/edit-file-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
2 | import React, { useEffect, useState } from 'react'
3 | import { BaseEditor } from '../base/base-editor'
4 | import { getProfileStr, setProfileStr } from '@renderer/utils/ipc'
5 | import { useNavigate } from 'react-router-dom'
6 | interface Props {
7 | id: string
8 | onClose: () => void
9 | }
10 | const EditFileModal: React.FC = (props) => {
11 | const { id, onClose } = props
12 | const [currData, setCurrData] = useState('')
13 | const navigate = useNavigate()
14 |
15 | const getContent = async (): Promise => {
16 | setCurrData(await getProfileStr(id))
17 | }
18 |
19 | useEffect(() => {
20 | getContent()
21 | }, [])
22 |
23 | return (
24 |
36 |
37 |
38 |
39 |
编辑订阅
40 |
41 | 注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用
42 |
53 | 功能
54 |
55 |
56 |
57 |
58 | setCurrData(value)} />
59 |
60 |
61 |
64 |
74 |
75 |
76 |
77 | )
78 | }
79 |
80 | export default EditFileModal
81 |
--------------------------------------------------------------------------------
/src/renderer/src/components/proxies/proxy-item.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardBody } from '@heroui/react'
2 | import { mihomoUnfixedProxy } from '@renderer/utils/ipc'
3 | import React, { useMemo, useState } from 'react'
4 | import { FaMapPin } from 'react-icons/fa6'
5 |
6 | interface Props {
7 | mutateProxies: () => void
8 | onProxyDelay: (proxy: string, url?: string) => Promise
9 | proxyDisplayMode: 'simple' | 'full'
10 | proxy: IMihomoProxy | IMihomoGroup
11 | group: IMihomoMixedGroup
12 | onSelect: (group: string, proxy: string) => void
13 | selected: boolean
14 | }
15 |
16 | const ProxyItem: React.FC = (props) => {
17 | const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay } = props
18 |
19 | const delay = useMemo(() => {
20 | if (proxy.history.length > 0) {
21 | return proxy.history[proxy.history.length - 1].delay
22 | }
23 | return -1
24 | }, [proxy])
25 |
26 | const [loading, setLoading] = useState(false)
27 | function delayColor(delay: number): 'primary' | 'success' | 'warning' | 'danger' {
28 | if (delay === -1) return 'primary'
29 | if (delay === 0) return 'danger'
30 | if (delay < 500) return 'success'
31 | return 'warning'
32 | }
33 |
34 | function delayText(delay: number): string {
35 | if (delay === -1) return '测试'
36 | if (delay === 0) return '超时'
37 | return delay.toString()
38 | }
39 |
40 | const onDelay = (): void => {
41 | setLoading(true)
42 | onProxyDelay(proxy.name, group.testUrl).finally(() => {
43 | mutateProxies()
44 | setLoading(false)
45 | })
46 | }
47 |
48 | const fixed = group.fixed && group.fixed === proxy.name
49 |
50 | return (
51 | onSelect(group.name, proxy.name)}
53 | isPressable
54 | fullWidth
55 | shadow="sm"
56 | className={`${fixed ? 'bg-secondary/30' : selected ? 'bg-primary/30' : 'bg-content2'}`}
57 | radius="sm"
58 | >
59 |
60 |
61 |
62 |
63 | {proxy.name}
64 |
65 | {proxyDisplayMode === 'full' && (
66 |
67 | {proxy.type}
68 |
69 | )}
70 |
71 |
72 | {fixed && (
73 |
86 | )}
87 |
98 |
99 |
100 |
101 |
102 | )
103 | }
104 |
105 | export default ProxyItem
106 |
--------------------------------------------------------------------------------
/src/renderer/src/components/resources/viewer.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
2 | import React, { useEffect, useState } from 'react'
3 | import { BaseEditor } from '../base/base-editor'
4 | import { getFileStr, setFileStr } from '@renderer/utils/ipc'
5 | import yaml from 'js-yaml'
6 | type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
7 |
8 | interface Props {
9 | onClose: () => void
10 | path: string
11 | type: string
12 | title: string
13 | privderType: string
14 | format?: string
15 | }
16 | const Viewer: React.FC = (props) => {
17 | const { type, path, title, format, privderType, onClose } = props
18 | const [currData, setCurrData] = useState('')
19 | let language: Language = !format || format === 'YamlRule' ? 'yaml' : 'text'
20 |
21 | const getContent = async (): Promise => {
22 | let fileContent: React.SetStateAction
23 | if (type === 'Inline') {
24 | fileContent = await getFileStr('config.yaml')
25 | language = 'yaml'
26 | } else {
27 | fileContent = await getFileStr(path)
28 | }
29 | try {
30 | const parsedYaml = yaml.load(fileContent)
31 | if (privderType === 'proxy-providers') {
32 | setCurrData(yaml.dump({
33 | 'proxies': parsedYaml[privderType][title].payload
34 | }))
35 | } else {
36 | setCurrData(yaml.dump({
37 | 'rules': parsedYaml[privderType][title].payload
38 | }))
39 | }
40 | } catch (error) {
41 | setCurrData(fileContent)
42 | }
43 | }
44 |
45 | useEffect(() => {
46 | getContent()
47 | }, [])
48 |
49 | return (
50 |
62 |
63 | {title}
64 |
65 | setCurrData(value)}
70 | />
71 |
72 |
73 |
76 | {type == 'File' && (
77 |
87 | )}
88 |
89 |
90 |
91 | )
92 | }
93 |
94 | export default Viewer
95 |
--------------------------------------------------------------------------------
/src/renderer/src/components/rules/rule-item.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardBody } from '@heroui/react'
2 | import React from 'react'
3 |
4 | const RuleItem: React.FC = (props) => {
5 | const { type, payload, proxy, index } = props
6 | return (
7 |
8 |
9 |
10 |
11 | {payload}
12 |
13 |
14 |
{type}
15 |
{proxy}
16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default RuleItem
24 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/css-editor-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
2 | import { BaseEditor } from '@renderer/components/base/base-editor'
3 | import { readTheme } from '@renderer/utils/ipc'
4 | import React, { useEffect, useState } from 'react'
5 | interface Props {
6 | theme: string
7 | onCancel: () => void
8 | onConfirm: (script: string) => void
9 | }
10 | const CSSEditorModal: React.FC = (props) => {
11 | const { theme, onCancel, onConfirm } = props
12 | const [currData, setCurrData] = useState('')
13 |
14 | useEffect(() => {
15 | if (theme) {
16 | readTheme(theme).then((css) => {
17 | setCurrData(css)
18 | })
19 | }
20 | }, [theme])
21 |
22 | return (
23 |
35 |
36 | 编辑主题
37 |
38 | setCurrData(value || '')}
42 | />
43 |
44 |
45 |
48 |
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | export default CSSEditorModal
58 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/sider-config.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import SettingCard from '../base/base-setting-card'
3 | import SettingItem from '../base/base-setting-item'
4 | import { RadioGroup, Radio } from '@heroui/react'
5 | import { useAppConfig } from '@renderer/hooks/use-app-config'
6 | const titleMap = {
7 | sysproxyCardStatus: '系统代理',
8 | tunCardStatus: '虚拟网卡',
9 | profileCardStatus: '订阅管理',
10 | proxyCardStatus: '代理组',
11 | ruleCardStatus: '规则',
12 | resourceCardStatus: '外部资源',
13 | overrideCardStatus: '覆写',
14 | connectionCardStatus: '连接',
15 | mihomoCoreCardStatus: '内核',
16 | dnsCardStatus: 'DNS',
17 | sniffCardStatus: '域名嗅探',
18 | logCardStatus: '日志',
19 | substoreCardStatus: 'Sub-Store'
20 | }
21 | const SiderConfig: React.FC = () => {
22 | const { appConfig, patchAppConfig } = useAppConfig()
23 | const {
24 | sysproxyCardStatus = 'col-span-1',
25 | tunCardStatus = 'col-span-1',
26 | profileCardStatus = 'col-span-2',
27 | proxyCardStatus = 'col-span-1',
28 | ruleCardStatus = 'col-span-1',
29 | resourceCardStatus = 'col-span-1',
30 | overrideCardStatus = 'col-span-1',
31 | connectionCardStatus = 'col-span-2',
32 | mihomoCoreCardStatus = 'col-span-2',
33 | dnsCardStatus = 'col-span-1',
34 | sniffCardStatus = 'col-span-1',
35 | logCardStatus = 'col-span-1',
36 | substoreCardStatus = 'col-span-1'
37 | } = appConfig || {}
38 |
39 | const cardStatus = {
40 | sysproxyCardStatus,
41 | tunCardStatus,
42 | profileCardStatus,
43 | proxyCardStatus,
44 | ruleCardStatus,
45 | resourceCardStatus,
46 | overrideCardStatus,
47 | connectionCardStatus,
48 | mihomoCoreCardStatus,
49 | dnsCardStatus,
50 | sniffCardStatus,
51 | logCardStatus,
52 | substoreCardStatus
53 | }
54 |
55 | return (
56 |
57 | {Object.keys(cardStatus).map((key, index, array) => {
58 | return (
59 |
60 | {
64 | patchAppConfig({ [key]: v as CardStatus })
65 | }}
66 | >
67 | 大
68 | 小
69 | 隐藏
70 |
71 |
72 | )
73 | })}
74 |
75 | )
76 | }
77 |
78 | export default SiderConfig
79 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/webdav-restore-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
2 | import { relaunchApp, webdavDelete, webdavRestore } from '@renderer/utils/ipc'
3 | import React, { useState } from 'react'
4 | import { MdDeleteForever } from 'react-icons/md'
5 | interface Props {
6 | filenames: string[]
7 | onClose: () => void
8 | }
9 | const WebdavRestoreModal: React.FC = (props) => {
10 | const { filenames: names, onClose } = props
11 | const [filenames, setFilenames] = useState(names)
12 | const [restoring, setRestoring] = useState(false)
13 |
14 | return (
15 |
23 |
24 | 恢复备份
25 |
26 | {filenames.length === 0 ? (
27 | 还没有备份
28 | ) : (
29 | filenames.map((filename) => (
30 |
31 |
50 |
66 |
67 | ))
68 | )}
69 |
70 |
71 |
74 |
75 |
76 |
77 | )
78 | }
79 |
80 | export default WebdavRestoreModal
81 |
--------------------------------------------------------------------------------
/src/renderer/src/components/sider/config-viewer.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | ModalContent,
4 | ModalHeader,
5 | ModalBody,
6 | ModalFooter,
7 | Button,
8 | Switch
9 | } from '@heroui/react'
10 | import React, { useEffect, useState, useCallback } from 'react'
11 | import { BaseEditor } from '../base/base-editor'
12 | import {
13 | getProfileConfig,
14 | getRawProfileStr,
15 | getRuntimeConfigStr,
16 | getCurrentProfileStr,
17 | getOverrideProfileStr
18 | } from '@renderer/utils/ipc'
19 | import useSWR from 'swr'
20 |
21 | interface Props {
22 | onClose: () => void
23 | }
24 | const ConfigViewer: React.FC = ({ onClose }) => {
25 | const [runtimeConfig, setRuntimeConfig] = useState('')
26 | const [rawProfile, setRawProfile] = useState('')
27 | const [profileConfig, setProfileConfig] = useState('')
28 | const [overrideConfig, setOverrideConfig] = useState('')
29 | const [isDiff, setIsDiff] = useState(false)
30 | const [isRaw, setIsRaw] = useState(false)
31 | const [isOverride, setIsOverride] = useState(false)
32 | const [sideBySide, setSideBySide] = useState(false)
33 |
34 | const { data: appConfig } = useSWR('getProfileConfig', getProfileConfig)
35 |
36 | const fetchConfigs = useCallback(async () => {
37 | setRuntimeConfig(await getRuntimeConfigStr())
38 | setRawProfile(await getRawProfileStr())
39 | setProfileConfig(await getCurrentProfileStr())
40 | setOverrideConfig(await getOverrideProfileStr())
41 | }, [appConfig])
42 |
43 | useEffect(() => {
44 | fetchConfigs()
45 | }, [fetchConfigs])
46 |
47 | return (
48 |
60 |
61 | 当前运行时配置
62 |
63 |
78 |
79 |
80 |
81 |
82 | 对比当前配置
83 |
84 | 侧边显示
85 | {
89 | setIsRaw(value)
90 | if (value) {
91 | setIsOverride(false)
92 | }
93 | }}
94 | />
95 | 显示原始文本
96 | {
100 | setIsOverride(value)
101 | if (value) {
102 | setIsRaw(false)
103 | }
104 | }}
105 | />
106 | 显示覆写后文本
107 |
108 |
111 |
112 |
113 |
114 | )
115 | }
116 |
117 | export default ConfigViewer
--------------------------------------------------------------------------------
/src/renderer/src/components/sider/dns-card.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
2 | import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
3 | import BorderSwitch from '@renderer/components/base/border-swtich'
4 | import { LuServer } from 'react-icons/lu'
5 | import { useLocation, useNavigate } from 'react-router-dom'
6 | import { patchMihomoConfig } from '@renderer/utils/ipc'
7 | import { useSortable } from '@dnd-kit/sortable'
8 | import { CSS } from '@dnd-kit/utilities'
9 | import { useAppConfig } from '@renderer/hooks/use-app-config'
10 | import React from 'react'
11 |
12 | interface Props {
13 | iconOnly?: boolean
14 | }
15 | const DNSCard: React.FC = (props) => {
16 | const { appConfig } = useAppConfig()
17 | const { iconOnly } = props
18 | const { dnsCardStatus = 'col-span-1', controlDns = true } = appConfig || {}
19 | const location = useLocation()
20 | const navigate = useNavigate()
21 | const match = location.pathname.includes('/dns')
22 | const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
23 | const { dns, tun } = controledMihomoConfig || {}
24 | const { enable = true } = dns || {}
25 | const {
26 | attributes,
27 | listeners,
28 | setNodeRef,
29 | transform: tf,
30 | transition,
31 | isDragging
32 | } = useSortable({
33 | id: 'dns'
34 | })
35 | const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
36 | const onChange = async (enable: boolean): Promise => {
37 | await patchControledMihomoConfig({ dns: { enable } })
38 | await patchMihomoConfig({ dns: { enable } })
39 | }
40 |
41 | if (iconOnly) {
42 | return (
43 |
44 |
45 |
56 |
57 |
58 | )
59 | }
60 |
61 | return (
62 |
71 |
78 |
79 |
80 |
90 |
96 |
97 |
98 |
99 |
102 | DNS
103 |
104 |
105 |
106 |
107 | )
108 | }
109 |
110 | export default DNSCard
111 |
--------------------------------------------------------------------------------
/src/renderer/src/components/sider/log-card.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
2 | import { IoJournalOutline } from 'react-icons/io5'
3 | import { useLocation, useNavigate } from 'react-router-dom'
4 | import { useSortable } from '@dnd-kit/sortable'
5 | import { CSS } from '@dnd-kit/utilities'
6 | import { useAppConfig } from '@renderer/hooks/use-app-config'
7 | import React from 'react'
8 |
9 | interface Props {
10 | iconOnly?: boolean
11 | }
12 |
13 | const LogCard: React.FC = (props) => {
14 | const { appConfig } = useAppConfig()
15 | const { iconOnly } = props
16 | const { logCardStatus = 'col-span-1' } = appConfig || {}
17 | const location = useLocation()
18 | const navigate = useNavigate()
19 | const match = location.pathname.includes('/logs')
20 | const {
21 | attributes,
22 | listeners,
23 | setNodeRef,
24 | transform: tf,
25 | transition,
26 | isDragging
27 | } = useSortable({
28 | id: 'log'
29 | })
30 | const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
31 |
32 | if (iconOnly) {
33 | return (
34 |
35 |
36 |
47 |
48 |
49 | )
50 | }
51 | return (
52 |
61 |
68 |
69 |
70 |
81 |
82 |
83 |
84 |
87 | 日志
88 |
89 |
90 |
91 |
92 | )
93 | }
94 |
95 | export default LogCard
96 |
--------------------------------------------------------------------------------
/src/renderer/src/components/sider/outbound-mode-switcher.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs, Tab } from '@heroui/react'
2 | import { useAppConfig } from '@renderer/hooks/use-app-config'
3 | import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
4 | import { useGroups } from '@renderer/hooks/use-groups'
5 | import { mihomoCloseAllConnections, patchMihomoConfig } from '@renderer/utils/ipc'
6 | import { Key } from 'react'
7 |
8 | interface Props {
9 | iconOnly?: boolean
10 | }
11 |
12 | const OutboundModeSwitcher: React.FC = (props) => {
13 | const { iconOnly } = props
14 | const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
15 | const { mutate: mutateGroups } = useGroups()
16 | const { appConfig } = useAppConfig()
17 | const { autoCloseConnection = true } = appConfig || {}
18 | const { mode } = controledMihomoConfig || {}
19 |
20 | const onChangeMode = async (mode: OutboundMode): Promise => {
21 | await patchControledMihomoConfig({ mode })
22 | await patchMihomoConfig({ mode })
23 | if (autoCloseConnection) {
24 | await mihomoCloseAllConnections()
25 | }
26 | mutateGroups()
27 | window.electron.ipcRenderer.send('updateTrayMenu')
28 | }
29 | if (!mode) return null
30 | if (iconOnly) {
31 | return (
32 | onChangeMode(key as OutboundMode)}
39 | >
40 |
41 |
42 |
43 |
44 | )
45 | }
46 | return (
47 | onChangeMode(key as OutboundMode)}
55 | >
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
63 | export default OutboundModeSwitcher
64 |
--------------------------------------------------------------------------------
/src/renderer/src/components/sider/override-card.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
2 | import React from 'react'
3 | import { MdFormatOverline } from 'react-icons/md'
4 | import { useLocation, useNavigate } from 'react-router-dom'
5 | import { useSortable } from '@dnd-kit/sortable'
6 | import { CSS } from '@dnd-kit/utilities'
7 | import { useAppConfig } from '@renderer/hooks/use-app-config'
8 |
9 | interface Props {
10 | iconOnly?: boolean
11 | }
12 |
13 | const OverrideCard: React.FC = (props) => {
14 | const { appConfig } = useAppConfig()
15 | const { iconOnly } = props
16 | const { overrideCardStatus = 'col-span-1' } = appConfig || {}
17 | const location = useLocation()
18 | const navigate = useNavigate()
19 | const match = location.pathname.includes('/override')
20 | const {
21 | attributes,
22 | listeners,
23 | setNodeRef,
24 | transform: tf,
25 | transition,
26 | isDragging
27 | } = useSortable({
28 | id: 'override'
29 | })
30 | const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
31 | if (iconOnly) {
32 | return (
33 |
34 |
35 |
46 |
47 |
48 | )
49 | }
50 | return (
51 |
60 |
67 |
68 |
69 |
80 |
81 |
82 |
83 |
86 | 覆写
87 |
88 |
89 |
90 |
91 | )
92 | }
93 |
94 | export default OverrideCard
95 |
--------------------------------------------------------------------------------
/src/renderer/src/components/sider/proxy-card.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardBody, CardFooter, Chip, Tooltip } from '@heroui/react'
2 | import { useSortable } from '@dnd-kit/sortable'
3 | import { CSS } from '@dnd-kit/utilities'
4 | import { LuGroup } from 'react-icons/lu'
5 | import { useLocation, useNavigate } from 'react-router-dom'
6 | import { useGroups } from '@renderer/hooks/use-groups'
7 | import { useAppConfig } from '@renderer/hooks/use-app-config'
8 | import React from 'react'
9 |
10 | interface Props {
11 | iconOnly?: boolean
12 | }
13 |
14 | const ProxyCard: React.FC = (props) => {
15 | const { appConfig } = useAppConfig()
16 | const { iconOnly } = props
17 | const { proxyCardStatus = 'col-span-1' } = appConfig || {}
18 | const location = useLocation()
19 | const navigate = useNavigate()
20 | const match = location.pathname.includes('/proxies')
21 | const { groups = [] } = useGroups()
22 | const {
23 | attributes,
24 | listeners,
25 | setNodeRef,
26 | transform: tf,
27 | transition,
28 | isDragging
29 | } = useSortable({
30 | id: 'proxy'
31 | })
32 | const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
33 |
34 | if (iconOnly) {
35 | return (
36 |
37 |
38 |
49 |
50 |
51 | )
52 | }
53 | return (
54 |
63 |
70 |
71 |
72 |
82 |
98 | {groups.length}
99 |
100 |
101 |
102 |
103 |
106 | 代理组
107 |
108 |
109 |
110 |
111 | )
112 | }
113 |
114 | export default ProxyCard
115 |
--------------------------------------------------------------------------------
/src/renderer/src/components/sider/resource-card.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
2 | import React from 'react'
3 | import { useLocation, useNavigate } from 'react-router-dom'
4 | import { useSortable } from '@dnd-kit/sortable'
5 | import { CSS } from '@dnd-kit/utilities'
6 | import { IoLayersOutline } from 'react-icons/io5'
7 | import { useAppConfig } from '@renderer/hooks/use-app-config'
8 |
9 | interface Props {
10 | iconOnly?: boolean
11 | }
12 |
13 | const ResourceCard: React.FC = (props) => {
14 | const { appConfig } = useAppConfig()
15 | const { iconOnly } = props
16 | const { resourceCardStatus = 'col-span-1' } = appConfig || {}
17 | const location = useLocation()
18 | const navigate = useNavigate()
19 | const match = location.pathname.includes('/resources')
20 | const {
21 | attributes,
22 | listeners,
23 | setNodeRef,
24 | transform: tf,
25 | transition,
26 | isDragging
27 | } = useSortable({
28 | id: 'resource'
29 | })
30 | const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
31 |
32 | if (iconOnly) {
33 | return (
34 |
35 |
36 |
47 |
48 |
49 | )
50 | }
51 | return (
52 |
61 |
68 |
69 |
70 |
81 |
82 |
83 |
84 |
87 | 外部资源
88 |
89 |
90 |
91 |
92 | )
93 | }
94 |
95 | export default ResourceCard
96 |
--------------------------------------------------------------------------------
/src/renderer/src/components/sider/rule-card.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardBody, CardFooter, Chip, Tooltip } from '@heroui/react'
2 | import { MdOutlineAltRoute } from 'react-icons/md'
3 | import { useLocation, useNavigate } from 'react-router-dom'
4 | import { useSortable } from '@dnd-kit/sortable'
5 | import { CSS } from '@dnd-kit/utilities'
6 | import { useRules } from '@renderer/hooks/use-rules'
7 | import { useAppConfig } from '@renderer/hooks/use-app-config'
8 | import React from 'react'
9 |
10 | interface Props {
11 | iconOnly?: boolean
12 | }
13 |
14 | const RuleCard: React.FC = (props) => {
15 | const { appConfig } = useAppConfig()
16 | const { iconOnly } = props
17 | const { ruleCardStatus = 'col-span-1' } = appConfig || {}
18 | const location = useLocation()
19 | const navigate = useNavigate()
20 | const match = location.pathname.includes('/rules')
21 | const { rules } = useRules()
22 | const {
23 | attributes,
24 | listeners,
25 | setNodeRef,
26 | transform: tf,
27 | transition,
28 | isDragging
29 | } = useSortable({
30 | id: 'rule'
31 | })
32 | const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
33 |
34 | if (iconOnly) {
35 | return (
36 |
37 |
38 |
49 |
50 |
51 | )
52 | }
53 | return (
54 |
63 |
70 |
71 |
72 |
83 |
99 | {rules?.rules?.length ?? 0}
100 |
101 |
102 |
103 |
104 |
107 | 规则
108 |
109 |
110 |
111 |
112 | )
113 | }
114 |
115 | export default RuleCard
116 |
--------------------------------------------------------------------------------
/src/renderer/src/components/sider/sniff-card.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
2 | import BorderSwitch from '@renderer/components/base/border-swtich'
3 | import { RiScan2Fill } from 'react-icons/ri'
4 | import { useLocation, useNavigate } from 'react-router-dom'
5 | import { patchMihomoConfig } from '@renderer/utils/ipc'
6 | import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
7 | import { useSortable } from '@dnd-kit/sortable'
8 | import { CSS } from '@dnd-kit/utilities'
9 | import { useAppConfig } from '@renderer/hooks/use-app-config'
10 | import React from 'react'
11 |
12 | interface Props {
13 | iconOnly?: boolean
14 | }
15 | const SniffCard: React.FC = (props) => {
16 | const { appConfig } = useAppConfig()
17 | const { iconOnly } = props
18 | const { sniffCardStatus = 'col-span-1', controlSniff = true } = appConfig || {}
19 | const location = useLocation()
20 | const navigate = useNavigate()
21 | const match = location.pathname.includes('/sniffer')
22 | const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
23 | const { sniffer } = controledMihomoConfig || {}
24 | const { enable } = sniffer || {}
25 | const {
26 | attributes,
27 | listeners,
28 | setNodeRef,
29 | transform: tf,
30 | transition,
31 | isDragging
32 | } = useSortable({
33 | id: 'sniff'
34 | })
35 | const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
36 | const onChange = async (enable: boolean): Promise => {
37 | await patchControledMihomoConfig({ sniffer: { enable } })
38 | await patchMihomoConfig({ sniffer: { enable } })
39 | }
40 |
41 | if (iconOnly) {
42 | return (
43 |
44 |
45 |
56 |
57 |
58 | )
59 | }
60 |
61 | return (
62 |
71 |
78 |
79 |
80 |
91 |
96 |
97 |
98 |
99 |
102 | 域名嗅探
103 |
104 |
105 |
106 |
107 | )
108 | }
109 |
110 | export default SniffCard
111 |
--------------------------------------------------------------------------------
/src/renderer/src/components/sider/substore-card.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
2 | import { useLocation, useNavigate } from 'react-router-dom'
3 | import { useSortable } from '@dnd-kit/sortable'
4 | import { CSS } from '@dnd-kit/utilities'
5 | import SubStoreIcon from '../base/substore-icon'
6 | import { useAppConfig } from '@renderer/hooks/use-app-config'
7 | import React from 'react'
8 |
9 | interface Props {
10 | iconOnly?: boolean
11 | }
12 |
13 | const SubStoreCard: React.FC = (props) => {
14 | const { appConfig } = useAppConfig()
15 | const { iconOnly } = props
16 | const { substoreCardStatus = 'col-span-1', useSubStore = true } = appConfig || {}
17 | const location = useLocation()
18 | const navigate = useNavigate()
19 | const match = location.pathname.includes('/substore')
20 | const {
21 | attributes,
22 | listeners,
23 | setNodeRef,
24 | transform: tf,
25 | transition,
26 | isDragging
27 | } = useSortable({
28 | id: 'substore'
29 | })
30 | const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
31 |
32 | if (iconOnly) {
33 | return (
34 |
35 |
36 |
47 |
48 |
49 | )
50 | }
51 |
52 | return (
53 |
62 |
69 |
70 |
71 |
81 |
82 |
83 |
84 |
87 | Sub-Store
88 |
89 |
90 |
91 |
92 | )
93 | }
94 |
95 | export default SubStoreCard
96 |
--------------------------------------------------------------------------------
/src/renderer/src/components/sysproxy/bypass-editor-modal.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import yaml from 'js-yaml'
3 | import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@heroui/react"
4 | import { BaseEditor } from "../base/base-editor"
5 |
6 | interface Props {
7 | bypass: string[]
8 | onCancel: () => void
9 | onConfirm: (bypass: string[]) => void
10 | }
11 |
12 | const ByPassEditorModal: React.FC = ({ bypass, onCancel, onConfirm }) => {
13 | const [currData, setCurrData] = useState('')
14 | useEffect(() => {
15 | setCurrData(yaml.dump({ bypass }))
16 | }, [bypass])
17 | const handleConfirm = () => {
18 | try {
19 | const parsed = yaml.load(currData)
20 | if (parsed && Array.isArray(parsed.bypass)) {
21 | onConfirm(parsed.bypass)
22 | } else {
23 | alert('YAML 格式错误')
24 | }
25 | } catch (e) {
26 | alert('YAML 解析失败: ' + e)
27 | }
28 | }
29 |
30 | return (
31 |
43 |
44 | 编辑绕过列表 (YAML)
45 |
46 | setCurrData(value || '')}
50 | />
51 |
52 |
53 |
56 |
59 |
60 |
61 |
62 | )
63 | }
64 |
65 | export default ByPassEditorModal
--------------------------------------------------------------------------------
/src/renderer/src/components/sysproxy/pac-editor-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
2 | import { BaseEditor } from '@renderer/components/base/base-editor'
3 | import React, { useState } from 'react'
4 | interface Props {
5 | script: string
6 | onCancel: () => void
7 | onConfirm: (script: string) => void
8 | }
9 | const PacEditorModal: React.FC = (props) => {
10 | const { script, onCancel, onConfirm } = props
11 | const [currData, setCurrData] = useState(script)
12 |
13 | return (
14 |
26 |
27 | 编辑PAC脚本
28 |
29 | setCurrData(value || '')}
33 | />
34 |
35 |
36 |
39 |
42 |
43 |
44 |
45 | )
46 | }
47 |
48 | export default PacEditorModal
49 |
--------------------------------------------------------------------------------
/src/renderer/src/components/updater/updater-button.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@heroui/react'
2 | import React, { useState } from 'react'
3 | import UpdaterModal from './updater-modal'
4 | import { GrUpgrade } from 'react-icons/gr'
5 |
6 | interface Props {
7 | iconOnly?: boolean
8 | latest?: {
9 | version: string
10 | changelog: string
11 | }
12 | }
13 |
14 | const UpdaterButton: React.FC = (props) => {
15 | const { iconOnly, latest } = props
16 | const [openModal, setOpenModal] = useState(false)
17 |
18 | if (!latest) return null
19 |
20 | return (
21 | <>
22 | {openModal && (
23 | {
27 | setOpenModal(false)
28 | }}
29 | />
30 | )}
31 | {iconOnly ? (
32 |
43 | ) : (
44 |
55 | )}
56 | >
57 | )
58 | }
59 |
60 | export default UpdaterButton
61 |
--------------------------------------------------------------------------------
/src/renderer/src/components/updater/updater-modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | ModalContent,
4 | ModalHeader,
5 | ModalBody,
6 | ModalFooter,
7 | Button,
8 | Code
9 | } from '@heroui/react'
10 | import ReactMarkdown from 'react-markdown'
11 | import React, { useState } from 'react'
12 | import { downloadAndInstallUpdate } from '@renderer/utils/ipc'
13 |
14 | interface Props {
15 | version: string
16 | changelog: string
17 | onClose: () => void
18 | }
19 | const UpdaterModal: React.FC = (props) => {
20 | const { version, changelog, onClose } = props
21 | const [downloading, setDownloading] = useState(false)
22 | const onUpdate = async (): Promise => {
23 | try {
24 | await downloadAndInstallUpdate(version)
25 | } catch (e) {
26 | alert(e)
27 | }
28 | }
29 |
30 | return (
31 |
39 |
40 |
41 | {version} 版本就绪
42 |
56 |
57 |
58 |
59 |
,
62 | code: ({ children }) => {children}
,
63 | h3: ({ ...props }) => ,
64 | li: ({ children }) => {children}
65 | }}
66 | >
67 | {changelog}
68 |
69 |
70 |
71 |
72 |
75 |
93 |
94 |
95 |
96 | )
97 | }
98 |
99 | export default UpdaterModal
100 |
--------------------------------------------------------------------------------
/src/renderer/src/floating.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
4 | import { HeroUIProvider } from '@heroui/react'
5 | import '@renderer/assets/floating.css'
6 | import FloatingApp from '@renderer/FloatingApp'
7 | import BaseErrorBoundary from './components/base/base-error-boundary'
8 | import { AppConfigProvider } from './hooks/use-app-config'
9 | import { ControledMihomoConfigProvider } from './hooks/use-controled-mihomo-config'
10 |
11 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 |
--------------------------------------------------------------------------------
/src/renderer/src/hooks/use-app-config.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, ReactNode } from 'react'
2 | import useSWR from 'swr'
3 | import { getAppConfig, patchAppConfig as patch } from '@renderer/utils/ipc'
4 |
5 | interface AppConfigContextType {
6 | appConfig: IAppConfig | undefined
7 | mutateAppConfig: () => void
8 | patchAppConfig: (value: Partial) => Promise
9 | }
10 |
11 | const AppConfigContext = createContext(undefined)
12 |
13 | export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
14 | const { data: appConfig, mutate: mutateAppConfig } = useSWR('getConfig', () => getAppConfig())
15 |
16 | const patchAppConfig = async (value: Partial): Promise => {
17 | try {
18 | await patch(value)
19 | } catch (e) {
20 | alert(e)
21 | } finally {
22 | mutateAppConfig()
23 | }
24 | }
25 |
26 | React.useEffect(() => {
27 | window.electron.ipcRenderer.on('appConfigUpdated', () => {
28 | mutateAppConfig()
29 | })
30 | return (): void => {
31 | window.electron.ipcRenderer.removeAllListeners('appConfigUpdated')
32 | }
33 | }, [])
34 |
35 | return (
36 |
37 | {children}
38 |
39 | )
40 | }
41 |
42 | export const useAppConfig = (): AppConfigContextType => {
43 | const context = useContext(AppConfigContext)
44 | if (context === undefined) {
45 | throw new Error('useAppConfig must be used within an AppConfigProvider')
46 | }
47 | return context
48 | }
49 |
--------------------------------------------------------------------------------
/src/renderer/src/hooks/use-controled-mihomo-config.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, ReactNode } from 'react'
2 | import useSWR from 'swr'
3 | import { getControledMihomoConfig, patchControledMihomoConfig as patch } from '@renderer/utils/ipc'
4 |
5 | interface ControledMihomoConfigContextType {
6 | controledMihomoConfig: Partial | undefined
7 | mutateControledMihomoConfig: () => void
8 | patchControledMihomoConfig: (value: Partial) => Promise
9 | }
10 |
11 | const ControledMihomoConfigContext = createContext(
12 | undefined
13 | )
14 |
15 | export const ControledMihomoConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
16 | const { data: controledMihomoConfig, mutate: mutateControledMihomoConfig } = useSWR(
17 | 'getControledMihomoConfig',
18 | () => getControledMihomoConfig()
19 | )
20 |
21 | const patchControledMihomoConfig = async (value: Partial): Promise => {
22 | try {
23 | await patch(value)
24 | } catch (e) {
25 | alert(e)
26 | } finally {
27 | mutateControledMihomoConfig()
28 | }
29 | }
30 |
31 | React.useEffect(() => {
32 | window.electron.ipcRenderer.on('controledMihomoConfigUpdated', () => {
33 | mutateControledMihomoConfig()
34 | })
35 | return (): void => {
36 | window.electron.ipcRenderer.removeAllListeners('controledMihomoConfigUpdated')
37 | }
38 | }, [])
39 |
40 | return (
41 |
44 | {children}
45 |
46 | )
47 | }
48 |
49 | export const useControledMihomoConfig = (): ControledMihomoConfigContextType => {
50 | const context = useContext(ControledMihomoConfigContext)
51 | if (context === undefined) {
52 | throw new Error('useControledMihomoConfig must be used within a ControledMihomoConfigProvider')
53 | }
54 | return context
55 | }
56 |
--------------------------------------------------------------------------------
/src/renderer/src/hooks/use-groups.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, ReactNode } from 'react'
2 | import useSWR from 'swr'
3 | import { mihomoGroups } from '@renderer/utils/ipc'
4 |
5 | interface GroupsContextType {
6 | groups: IMihomoMixedGroup[] | undefined
7 | mutate: () => void
8 | }
9 |
10 | const GroupsContext = createContext(undefined)
11 |
12 | export const GroupsProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
13 | const { data: groups, mutate } = useSWR('mihomoGroups', mihomoGroups, {
14 | errorRetryInterval: 200,
15 | errorRetryCount: 10
16 | })
17 |
18 | React.useEffect(() => {
19 | window.electron.ipcRenderer.on('groupsUpdated', () => {
20 | mutate()
21 | })
22 | return (): void => {
23 | window.electron.ipcRenderer.removeAllListeners('groupsUpdated')
24 | }
25 | }, [])
26 |
27 | return {children}
28 | }
29 |
30 | export const useGroups = (): GroupsContextType => {
31 | const context = useContext(GroupsContext)
32 | if (context === undefined) {
33 | throw new Error('useGroups must be used within an GroupsProvider')
34 | }
35 | return context
36 | }
37 |
--------------------------------------------------------------------------------
/src/renderer/src/hooks/use-override-config.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, ReactNode } from 'react'
2 | import useSWR from 'swr'
3 | import {
4 | getOverrideConfig,
5 | setOverrideConfig as set,
6 | addOverrideItem as add,
7 | removeOverrideItem as remove,
8 | updateOverrideItem as update
9 | } from '@renderer/utils/ipc'
10 |
11 | interface OverrideConfigContextType {
12 | overrideConfig: IOverrideConfig | undefined
13 | setOverrideConfig: (config: IOverrideConfig) => Promise
14 | mutateOverrideConfig: () => void
15 | addOverrideItem: (item: Partial) => Promise
16 | updateOverrideItem: (item: IOverrideItem) => Promise
17 | removeOverrideItem: (id: string) => Promise
18 | }
19 |
20 | const OverrideConfigContext = createContext(undefined)
21 |
22 | export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
23 | const { data: overrideConfig, mutate: mutateOverrideConfig } = useSWR('getOverrideConfig', () =>
24 | getOverrideConfig()
25 | )
26 |
27 | const setOverrideConfig = async (config: IOverrideConfig): Promise => {
28 | try {
29 | await set(config)
30 | } catch (e) {
31 | alert(e)
32 | } finally {
33 | mutateOverrideConfig()
34 | }
35 | }
36 |
37 | const addOverrideItem = async (item: Partial): Promise => {
38 | try {
39 | await add(item)
40 | } catch (e) {
41 | alert(e)
42 | } finally {
43 | mutateOverrideConfig()
44 | }
45 | }
46 |
47 | const removeOverrideItem = async (id: string): Promise => {
48 | try {
49 | await remove(id)
50 | } catch (e) {
51 | alert(e)
52 | } finally {
53 | mutateOverrideConfig()
54 | }
55 | }
56 |
57 | const updateOverrideItem = async (item: IOverrideItem): Promise => {
58 | try {
59 | await update(item)
60 | } catch (e) {
61 | alert(e)
62 | } finally {
63 | mutateOverrideConfig()
64 | }
65 | }
66 |
67 | return (
68 |
78 | {children}
79 |
80 | )
81 | }
82 |
83 | export const useOverrideConfig = (): OverrideConfigContextType => {
84 | const context = useContext(OverrideConfigContext)
85 | if (context === undefined) {
86 | throw new Error('useOverrideConfig must be used within an OverrideConfigProvider')
87 | }
88 | return context
89 | }
90 |
--------------------------------------------------------------------------------
/src/renderer/src/hooks/use-profile-config.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, ReactNode } from 'react'
2 | import useSWR from 'swr'
3 | import {
4 | getProfileConfig,
5 | setProfileConfig as set,
6 | addProfileItem as add,
7 | removeProfileItem as remove,
8 | updateProfileItem as update,
9 | changeCurrentProfile as change
10 | } from '@renderer/utils/ipc'
11 |
12 | interface ProfileConfigContextType {
13 | profileConfig: IProfileConfig | undefined
14 | setProfileConfig: (config: IProfileConfig) => Promise
15 | mutateProfileConfig: () => void
16 | addProfileItem: (item: Partial) => Promise
17 | updateProfileItem: (item: IProfileItem) => Promise
18 | removeProfileItem: (id: string) => Promise
19 | changeCurrentProfile: (id: string) => Promise
20 | }
21 |
22 | const ProfileConfigContext = createContext(undefined)
23 |
24 | export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
25 | const { data: profileConfig, mutate: mutateProfileConfig } = useSWR('getProfileConfig', () =>
26 | getProfileConfig()
27 | )
28 |
29 | const setProfileConfig = async (config: IProfileConfig): Promise => {
30 | try {
31 | await set(config)
32 | } catch (e) {
33 | alert(e)
34 | } finally {
35 | mutateProfileConfig()
36 | window.electron.ipcRenderer.send('updateTrayMenu')
37 | }
38 | }
39 |
40 | const addProfileItem = async (item: Partial): Promise => {
41 | try {
42 | await add(item)
43 | } catch (e) {
44 | alert(e)
45 | } finally {
46 | mutateProfileConfig()
47 | window.electron.ipcRenderer.send('updateTrayMenu')
48 | }
49 | }
50 |
51 | const removeProfileItem = async (id: string): Promise => {
52 | try {
53 | await remove(id)
54 | } catch (e) {
55 | alert(e)
56 | } finally {
57 | mutateProfileConfig()
58 | window.electron.ipcRenderer.send('updateTrayMenu')
59 | }
60 | }
61 |
62 | const updateProfileItem = async (item: IProfileItem): Promise => {
63 | try {
64 | await update(item)
65 | } catch (e) {
66 | alert(e)
67 | } finally {
68 | mutateProfileConfig()
69 | window.electron.ipcRenderer.send('updateTrayMenu')
70 | }
71 | }
72 |
73 | const changeCurrentProfile = async (id: string): Promise => {
74 | try {
75 | await change(id)
76 | } catch (e) {
77 | alert(e)
78 | } finally {
79 | mutateProfileConfig()
80 | window.electron.ipcRenderer.send('updateTrayMenu')
81 | }
82 | }
83 |
84 | React.useEffect(() => {
85 | window.electron.ipcRenderer.on('profileConfigUpdated', () => {
86 | mutateProfileConfig()
87 | })
88 | return (): void => {
89 | window.electron.ipcRenderer.removeAllListeners('profileConfigUpdated')
90 | }
91 | }, [])
92 |
93 | return (
94 |
105 | {children}
106 |
107 | )
108 | }
109 |
110 | export const useProfileConfig = (): ProfileConfigContextType => {
111 | const context = useContext(ProfileConfigContext)
112 | if (context === undefined) {
113 | throw new Error('useProfileConfig must be used within a ProfileConfigProvider')
114 | }
115 | return context
116 | }
117 |
--------------------------------------------------------------------------------
/src/renderer/src/hooks/use-rules.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, ReactNode } from 'react'
2 | import useSWR from 'swr'
3 | import { mihomoRules } from '@renderer/utils/ipc'
4 |
5 | interface RulesContextType {
6 | rules: IMihomoRulesInfo | undefined
7 | mutate: () => void
8 | }
9 |
10 | const RulesContext = createContext(undefined)
11 |
12 | export const RulesProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
13 | const { data: rules, mutate } = useSWR('mihomoRules', mihomoRules, {
14 | errorRetryInterval: 200,
15 | errorRetryCount: 10
16 | })
17 |
18 | React.useEffect(() => {
19 | window.electron.ipcRenderer.on('rulesUpdated', () => {
20 | mutate()
21 | })
22 | return (): void => {
23 | window.electron.ipcRenderer.removeAllListeners('rulesUpdated')
24 | }
25 | }, [])
26 |
27 | return {children}
28 | }
29 |
30 | export const useRules = (): RulesContextType => {
31 | const context = useContext(RulesContext)
32 | if (context === undefined) {
33 | throw new Error('useRules must be used within an RulesProvider')
34 | }
35 | return context
36 | }
37 |
--------------------------------------------------------------------------------
/src/renderer/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import { HashRouter } from 'react-router-dom'
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
5 | import { HeroUIProvider } from '@heroui/react'
6 | import { init, platform } from '@renderer/utils/init'
7 | import '@renderer/assets/main.css'
8 | import App from '@renderer/App'
9 | import BaseErrorBoundary from './components/base/base-error-boundary'
10 | import { openDevTools, quitApp } from './utils/ipc'
11 | import { AppConfigProvider } from './hooks/use-app-config'
12 | import { ControledMihomoConfigProvider } from './hooks/use-controled-mihomo-config'
13 | import { OverrideConfigProvider } from './hooks/use-override-config'
14 | import { ProfileConfigProvider } from './hooks/use-profile-config'
15 | import { RulesProvider } from './hooks/use-rules'
16 | import { GroupsProvider } from './hooks/use-groups'
17 |
18 | let F12Count = 0
19 |
20 | init().then(() => {
21 | document.addEventListener('keydown', (e) => {
22 | if (platform !== 'darwin' && e.ctrlKey && e.key === 'q') {
23 | e.preventDefault()
24 | quitApp()
25 | }
26 | if (platform === 'darwin' && e.metaKey && e.key === 'q') {
27 | e.preventDefault()
28 | quitApp()
29 | }
30 | if (e.key === 'Escape') {
31 | e.preventDefault()
32 | window.close()
33 | }
34 | if (e.key === 'F12') {
35 | e.preventDefault()
36 | F12Count++
37 | if (F12Count >= 5) {
38 | openDevTools()
39 | F12Count = 0
40 | }
41 | }
42 | })
43 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | )
68 | })
69 |
--------------------------------------------------------------------------------
/src/renderer/src/pages/logs.tsx:
--------------------------------------------------------------------------------
1 | import BasePage from '@renderer/components/base/base-page'
2 | import LogItem from '@renderer/components/logs/log-item'
3 | import { useEffect, useMemo, useRef, useState } from 'react'
4 | import { Button, Divider, Input } from '@heroui/react'
5 | import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
6 | import { IoLocationSharp } from 'react-icons/io5'
7 | import { CgTrash } from 'react-icons/cg'
8 |
9 | import { includesIgnoreCase } from '@renderer/utils/includes'
10 |
11 | const cachedLogs: {
12 | log: IMihomoLogInfo[]
13 | trigger: ((i: IMihomoLogInfo[]) => void) | null
14 | clean: () => void
15 | } = {
16 | log: [],
17 | trigger: null,
18 | clean(): void {
19 | this.log = []
20 | if (this.trigger !== null) {
21 | this.trigger(this.log)
22 | }
23 | }
24 | }
25 |
26 | window.electron.ipcRenderer.on('mihomoLogs', (_e, log: IMihomoLogInfo) => {
27 | log.time = new Date().toLocaleString()
28 | cachedLogs.log.push(log)
29 | if (cachedLogs.log.length >= 500) {
30 | cachedLogs.log.shift()
31 | }
32 | if (cachedLogs.trigger !== null) {
33 | cachedLogs.trigger(cachedLogs.log)
34 | }
35 | })
36 |
37 | const Logs: React.FC = () => {
38 | const [logs, setLogs] = useState(cachedLogs.log)
39 | const [filter, setFilter] = useState('')
40 | const [trace, setTrace] = useState(true)
41 |
42 | const virtuosoRef = useRef(null)
43 | const filteredLogs = useMemo(() => {
44 | if (filter === '') return logs
45 | return logs.filter((log) => {
46 | return includesIgnoreCase(log.payload, filter) || includesIgnoreCase(log.type, filter)
47 | })
48 | }, [logs, filter])
49 |
50 | useEffect(() => {
51 | if (!trace) return
52 | virtuosoRef.current?.scrollToIndex({
53 | index: filteredLogs.length - 1,
54 | behavior: 'smooth',
55 | align: 'end',
56 | offset: 0
57 | })
58 | }, [filteredLogs, trace])
59 |
60 | useEffect(() => {
61 | const old = cachedLogs.trigger
62 | cachedLogs.trigger = (a): void => {
63 | setLogs([...a])
64 | }
65 | return (): void => {
66 | cachedLogs.trigger = old
67 | }
68 | }, [])
69 |
70 | return (
71 |
72 |
73 |
74 |
81 |
93 |
106 |
107 |
108 |
109 |
110 | {
116 | return (
117 |
124 | )
125 | }}
126 | />
127 |
128 |
129 | )
130 | }
131 |
132 | export default Logs
133 |
--------------------------------------------------------------------------------
/src/renderer/src/pages/resources.tsx:
--------------------------------------------------------------------------------
1 | import BasePage from '@renderer/components/base/base-page'
2 | import GeoData from '@renderer/components/resources/geo-data'
3 | import ProxyProvider from '@renderer/components/resources/proxy-provider'
4 | import RuleProvider from '@renderer/components/resources/rule-provider'
5 | const Resources: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
15 | export default Resources
16 |
--------------------------------------------------------------------------------
/src/renderer/src/pages/rules.tsx:
--------------------------------------------------------------------------------
1 | import BasePage from '@renderer/components/base/base-page'
2 | import RuleItem from '@renderer/components/rules/rule-item'
3 | import { Virtuoso } from 'react-virtuoso'
4 | import { useMemo, useState } from 'react'
5 | import { Divider, Input } from '@heroui/react'
6 | import { useRules } from '@renderer/hooks/use-rules'
7 | import { includesIgnoreCase } from '@renderer/utils/includes'
8 |
9 | const Rules: React.FC = () => {
10 | const { rules } = useRules()
11 | const [filter, setFilter] = useState('')
12 |
13 | const filteredRules = useMemo(() => {
14 | if (!rules) return []
15 | if (filter === '') return rules.rules
16 | return rules.rules.filter((rule) => {
17 | return (
18 | includesIgnoreCase(rule.payload, filter) ||
19 | includesIgnoreCase(rule.type, filter) ||
20 | includesIgnoreCase(rule.proxy, filter)
21 | )
22 | })
23 | }, [rules, filter])
24 |
25 | return (
26 |
27 |
39 |
40 | (
43 |
50 | )}
51 | />
52 |
53 |
54 | )
55 | }
56 |
57 | export default Rules
58 |
--------------------------------------------------------------------------------
/src/renderer/src/pages/settings.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@heroui/react'
2 | import BasePage from '@renderer/components/base/base-page'
3 | // import { CgWebsite } from 'react-icons/cg'
4 | import { IoLogoGithub } from 'react-icons/io5'
5 | import WebdavConfig from '@renderer/components/settings/webdav-config'
6 | import GeneralConfig from '@renderer/components/settings/general-config'
7 | import MihomoConfig from '@renderer/components/settings/mihomo-config'
8 | import Actions from '@renderer/components/settings/actions'
9 | import ShortcutConfig from '@renderer/components/settings/shortcut-config'
10 | import { FaTelegramPlane } from 'react-icons/fa'
11 | import SiderConfig from '@renderer/components/settings/sider-config'
12 | import SubStoreConfig from '@renderer/components/settings/substore-config'
13 | import AppearanceConfig from '@renderer/components/settings/appearance-confis'
14 |
15 | const Settings: React.FC = () => {
16 | return (
17 |
21 | {/* */}
33 |
45 |
57 | >
58 | }
59 | >
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | )
70 | }
71 |
72 | export default Settings
73 |
--------------------------------------------------------------------------------
/src/renderer/src/pages/substore.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@heroui/react'
2 | import BasePage from '@renderer/components/base/base-page'
3 | import { useAppConfig } from '@renderer/hooks/use-app-config'
4 | import {
5 | subStoreFrontendPort,
6 | subStorePort,
7 | startSubStoreFrontendServer,
8 | startSubStoreBackendServer,
9 | stopSubStoreFrontendServer,
10 | stopSubStoreBackendServer,
11 | downloadSubStore
12 | } from '@renderer/utils/ipc'
13 | import React, { useEffect, useState } from 'react'
14 | import { HiExternalLink } from 'react-icons/hi'
15 | import { IoMdCloudDownload } from 'react-icons/io'
16 |
17 | const SubStore: React.FC = () => {
18 | const { appConfig } = useAppConfig()
19 | const { useCustomSubStore, customSubStoreUrl } = appConfig || {}
20 | const [backendPort, setBackendPort] = useState()
21 | const [frontendPort, setFrontendPort] = useState()
22 | const [isUpdating, setIsUpdating] = useState(false)
23 | const getPort = async (): Promise => {
24 | setBackendPort(await subStorePort())
25 | setFrontendPort(await subStoreFrontendPort())
26 | }
27 | useEffect(() => {
28 | getPort()
29 | }, [useCustomSubStore])
30 |
31 | if (!useCustomSubStore && !backendPort) return null
32 | if (!frontendPort) return null
33 | return (
34 | <>
35 |
39 |
68 |
82 |
83 | }
84 | >
85 |
90 |
91 | >
92 | )
93 | }
94 |
95 | export default SubStore
96 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom'
2 | import Override from '@renderer/pages/override'
3 | import Proxies from '@renderer/pages/proxies'
4 | import Rules from '@renderer/pages/rules'
5 | import Settings from '@renderer/pages/settings'
6 | import Profiles from '@renderer/pages/profiles'
7 | import Logs from '@renderer/pages/logs'
8 | import Connections from '@renderer/pages/connections'
9 | import Mihomo from '@renderer/pages/mihomo'
10 | import Sysproxy from '@renderer/pages/syspeoxy'
11 | import Tun from '@renderer/pages/tun'
12 | import Resources from '@renderer/pages/resources'
13 | import DNS from '@renderer/pages/dns'
14 | import Sniffer from '@renderer/pages/sniffer'
15 | import SubStore from '@renderer/pages/substore'
16 | const routes = [
17 | {
18 | path: '/mihomo',
19 | element:
20 | },
21 | {
22 | path: '/sysproxy',
23 | element:
24 | },
25 | {
26 | path: '/tun',
27 | element:
28 | },
29 | {
30 | path: '/proxies',
31 | element:
32 | },
33 | {
34 | path: '/rules',
35 | element:
36 | },
37 | {
38 | path: '/resources',
39 | element:
40 | },
41 | {
42 | path: '/dns',
43 | element:
44 | },
45 | {
46 | path: '/sniffer',
47 | element:
48 | },
49 | {
50 | path: '/logs',
51 | element:
52 | },
53 | {
54 | path: '/connections',
55 | element:
56 | },
57 | {
58 | path: '/override',
59 | element:
60 | },
61 | {
62 | path: '/profiles',
63 | element:
64 | },
65 | {
66 | path: '/settings',
67 | element:
68 | },
69 | {
70 | path: '/substore',
71 | element:
72 | },
73 | {
74 | path: '/',
75 | element:
76 | }
77 | ]
78 |
79 | export default routes
80 |
--------------------------------------------------------------------------------
/src/renderer/src/utils/calc.ts:
--------------------------------------------------------------------------------
1 | export function calcTraffic(byte: number): string {
2 | if (byte < 1024) return `${formatNumString(byte)} B`
3 | byte /= 1024
4 | if (byte < 1024) return `${formatNumString(byte)} KB`
5 | byte /= 1024
6 | if (byte < 1024) return `${formatNumString(byte)} MB`
7 | byte /= 1024
8 | if (byte < 1024) return `${formatNumString(byte)} GB`
9 | byte /= 1024
10 | if (byte < 1024) return `${formatNumString(byte)} TB`
11 | byte /= 1024
12 | if (byte < 1024) return `${formatNumString(byte)} PB`
13 | byte /= 1024
14 | if (byte < 1024) return `${formatNumString(byte)} EB`
15 | byte /= 1024
16 | if (byte < 1024) return `${formatNumString(byte)} ZB`
17 | byte /= 1024
18 | return `${formatNumString(byte)} YB`
19 | }
20 |
21 | function formatNumString(num: number): string {
22 | let str = num.toFixed(2)
23 | if (str.length <= 5) return str
24 | if (str.length == 6) {
25 | str = num.toFixed(1)
26 | return str
27 | } else {
28 | str = Math.round(num).toString()
29 | return str
30 | }
31 | }
32 |
33 | export function calcPercent(
34 | upload: number | undefined,
35 | download: number | undefined,
36 | total: number | undefined
37 | ): number {
38 | if (upload === undefined || download === undefined || total === undefined) {
39 | return 100
40 | }
41 | return Math.round(((upload + download) / total) * 100)
42 | }
43 |
--------------------------------------------------------------------------------
/src/renderer/src/utils/debounce.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | export default function debounce void>(func: T, wait: number): T {
3 | let timeout: ReturnType | null = null
4 | return function (this: any, ...args: Parameters) {
5 | if (timeout !== null) {
6 | clearTimeout(timeout)
7 | }
8 | timeout = setTimeout(() => func.apply(this, args), wait)
9 | } as T
10 | }
11 |
--------------------------------------------------------------------------------
/src/renderer/src/utils/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/renderer/src/utils/hash.ts:
--------------------------------------------------------------------------------
1 | import { MD5 } from 'crypto-js'
2 |
3 | export class HashType {
4 | private hashValue: string
5 |
6 | constructor(hash: string) {
7 | this.hashValue = hash
8 | }
9 |
10 | static makeHash(data: string): HashType {
11 | const hash = MD5(data).toString()
12 | return new HashType(hash)
13 | }
14 |
15 | equal(hash: HashType): boolean {
16 | return this.hashValue === hash.hashValue
17 | }
18 |
19 | toString(): string {
20 | return this.hashValue
21 | }
22 |
23 | isValid(): boolean {
24 | return this.hashValue.length === 32
25 | }
26 | }
27 |
28 | export function getHash(name: string): string {
29 | const hash = HashType.makeHash(name)
30 | return hash.toString()
31 | }
32 |
--------------------------------------------------------------------------------
/src/renderer/src/utils/includes.ts:
--------------------------------------------------------------------------------
1 | export function includesIgnoreCase(mainStr: string = '', subStr: string = ''): boolean {
2 | return mainStr.toLowerCase().includes(subStr.toLowerCase())
3 | }
4 |
--------------------------------------------------------------------------------
/src/renderer/src/utils/init.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | import { getPlatform, getVersion } from './ipc'
4 | // const originError = console.error
5 | // const originWarn = console.warn
6 | // console.error = function (...args: any[]): void {
7 | // if (typeof args[0] === 'string' && args[0].includes('validateDOMNesting')) {
8 | // return
9 | // }
10 | // originError.call(console, args)
11 | // }
12 | // console.warn = function (...args): void {
13 | // if (typeof args[0] === 'string' && args[0].includes('aria-label')) {
14 | // return
15 | // }
16 | // originWarn.call(console, args)
17 | // }
18 |
19 | export let platform: NodeJS.Platform
20 | export let version: string
21 |
22 | export async function init(): Promise {
23 | platform = await getPlatform()
24 | version = await getVersion()
25 | }
26 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | const {heroui} = require("@heroui/react")
3 |
4 | module.exports = {
5 | content: [
6 | './src/renderer/src/**/*.{js,ts,jsx,tsx}',
7 | './node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'
8 | ],
9 | theme: {
10 | extend: {}
11 | },
12 | darkMode: 'class',
13 | plugins: [heroui()]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
3 | "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*.d.ts"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "types": ["electron-vite/node"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.web.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
3 | "include": [
4 | "src/renderer/src/utils/env.d.ts",
5 | "src/renderer/src/**/*",
6 | "src/renderer/src/**/*.tsx",
7 | "src/preload/*.d.ts",
8 | "src/shared/*.d.ts"
9 | ],
10 | "compilerOptions": {
11 | "composite": true,
12 | "jsx": "react-jsx",
13 | "baseUrl": ".",
14 | "paths": {
15 | "@renderer/*": [
16 | "src/renderer/src/*"
17 | ]
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------