mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-28 13:40:29 +08:00
Compare commits
291 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
364578f210 | ||
|
|
674cefcc29 | ||
|
|
827e744601 | ||
|
|
151726fcce | ||
|
|
6f3845151d | ||
|
|
a9b5887e15 | ||
|
|
727fd48684 | ||
|
|
e2ab88f4e2 | ||
|
|
1a5c001dbd | ||
|
|
6744e14c66 | ||
|
|
62a04cc5ad | ||
|
|
555130001b | ||
|
|
78f9211ebe | ||
|
|
c6d0e05851 | ||
|
|
caf962f921 | ||
|
|
0b8c77d200 | ||
|
|
d6e456302e | ||
|
|
daa8e7ba7e | ||
|
|
27ab6d1b5c | ||
|
|
da5336ee36 | ||
|
|
bdb7fb6489 | ||
|
|
1a1992c617 | ||
|
|
511eb0c7fa | ||
|
|
f54ffcf42b | ||
|
|
5a84d6485e | ||
|
|
afe93774b0 | ||
|
|
00be605e6e | ||
|
|
944475d791 | ||
|
|
08f15c7e01 | ||
|
|
0218f72c5f | ||
|
|
c54ce3577d | ||
|
|
6cb432dea9 | ||
|
|
a6a3afd3bb | ||
|
|
a2faf0fc8f | ||
|
|
b15fc6ce3a | ||
|
|
fcb323a17a | ||
|
|
69e65a3959 | ||
|
|
6ffcf544b8 | ||
|
|
36746074da | ||
|
|
d763e93984 | ||
|
|
6ab920ddbd | ||
|
|
59bd7e8a08 | ||
|
|
f30596231b | ||
|
|
4b238d4dc2 | ||
|
|
81bb2c44e0 | ||
|
|
6cf1ae2c25 | ||
|
|
c906a10562 | ||
|
|
eb12f13525 | ||
|
|
502c089f86 | ||
|
|
83c5b03773 | ||
|
|
55a3bc9cd3 | ||
|
|
95df5e7712 | ||
|
|
928bae7b26 | ||
|
|
356914a5ed | ||
|
|
b102c4c59b | ||
|
|
c60b041192 | ||
|
|
4da4c242ad | ||
|
|
8d88851444 | ||
|
|
d28d33849c | ||
|
|
a187bc7362 | ||
|
|
9d84c6ac9b | ||
|
|
bb6021877d | ||
|
|
da3f3878f1 | ||
|
|
32d91335cc | ||
|
|
f2a4049f6c | ||
|
|
4a48445043 | ||
|
|
a69d24b29c | ||
|
|
9acab62d1f | ||
|
|
116c83ee38 | ||
|
|
0c1d259332 | ||
|
|
8210b477ab | ||
|
|
fa3c412146 | ||
|
|
31190e169f | ||
|
|
4b7ab042f3 | ||
|
|
7c8661ec0b | ||
|
|
35c239e02d | ||
|
|
77b67849ed | ||
|
|
e746ab4241 | ||
|
|
26a95bdb89 | ||
|
|
749ac64698 | ||
|
|
df7066a5f4 | ||
|
|
d733141e2a | ||
|
|
671892d6a3 | ||
|
|
8f426d7aaf | ||
|
|
dd62627811 | ||
|
|
1d7e907c87 | ||
|
|
e6b5cd8d72 | ||
|
|
aee24a9d86 | ||
|
|
857132fcdb | ||
|
|
bf97b5e03f | ||
|
|
c19fb6d989 | ||
|
|
7cece23769 | ||
|
|
7d70008148 | ||
|
|
c7e252d418 | ||
|
|
c8269c3496 | ||
|
|
ed9963c0a5 | ||
|
|
0b06d2ac9d | ||
|
|
0c345faddb | ||
|
|
7b28f9253b | ||
|
|
1e3febdc80 | ||
|
|
07a1730615 | ||
|
|
1166e88f67 | ||
|
|
182f2375f1 | ||
|
|
09921d984a | ||
|
|
04aad7fd40 | ||
|
|
2b42f68a30 | ||
|
|
ceb89d28c8 | ||
|
|
6d1fe87fe2 | ||
|
|
8db77267d3 | ||
|
|
f1cfa97c39 | ||
|
|
01c2b96acb | ||
|
|
56139a5e9f | ||
|
|
22c35b606e | ||
|
|
ec3efe89c7 | ||
|
|
c338051802 | ||
|
|
e2653170c0 | ||
|
|
7b1fc24be4 | ||
|
|
b411a8fb51 | ||
|
|
f004921ca0 | ||
|
|
4c51a4a611 | ||
|
|
148f4f217e | ||
|
|
9e078d78a5 | ||
|
|
bb437f96a7 | ||
|
|
fddb641945 | ||
|
|
75e754f03b | ||
|
|
13f8705dc6 | ||
|
|
2faa1659dc | ||
|
|
5bafb7ccc1 | ||
|
|
9f6aac708b | ||
|
|
dac2336eb6 | ||
|
|
fe163c40c2 | ||
|
|
f85e1ecd55 | ||
|
|
955b3fe666 | ||
|
|
4ce63aec7e | ||
|
|
5308db9209 | ||
|
|
94cc2f59c8 | ||
|
|
94c126b601 | ||
|
|
b38bd8d1f8 | ||
|
|
74f56ae49d | ||
|
|
c0d3adfe21 | ||
|
|
700c5d26b9 | ||
|
|
c043281b9f | ||
|
|
d02421414f | ||
|
|
f20f15df85 | ||
|
|
181ac91421 | ||
|
|
5a525aee2b | ||
|
|
57142e2181 | ||
|
|
23e773ded2 | ||
|
|
35d2a10a62 | ||
|
|
8abfeb1213 | ||
|
|
9a860cbe94 | ||
|
|
5c2715b989 | ||
|
|
56ba9a1bc9 | ||
|
|
e4fab9d5e8 | ||
|
|
82c4708636 | ||
|
|
55cc48d9d7 | ||
|
|
d06f70ccb9 | ||
|
|
742f044cee | ||
|
|
975c8db829 | ||
|
|
acde7a55c4 | ||
|
|
4cea742bc0 | ||
|
|
128c3ababa | ||
|
|
c4f7861870 | ||
|
|
6e4a87fda8 | ||
|
|
cd0a9dd911 | ||
|
|
1b4457eee9 | ||
|
|
cb44c13d7a | ||
|
|
d6ce915161 | ||
|
|
7df1894af3 | ||
|
|
30bf7567fb | ||
|
|
911d72975e | ||
|
|
cebece3748 | ||
|
|
e0612344d7 | ||
|
|
f193dbe696 | ||
|
|
a5f825d5ca | ||
|
|
7078f4a5ab | ||
|
|
b6dc50b7ae | ||
|
|
82d7cf05bd | ||
|
|
95f48a5d4d | ||
|
|
d731c21804 | ||
|
|
338c0193f3 | ||
|
|
2d8560351e | ||
|
|
75b2df4de1 | ||
|
|
dc8ffb79c6 | ||
|
|
dc9a41ed04 | ||
|
|
caefbe915c | ||
|
|
2164aa7520 | ||
|
|
64ed2864dc | ||
|
|
3b139263ed | ||
|
|
697319b953 | ||
|
|
2705bba00f | ||
|
|
636a59ec91 | ||
|
|
195ed68152 | ||
|
|
ee0a0b4867 | ||
|
|
710780456a | ||
|
|
ea5397f250 | ||
|
|
dcb59e767c | ||
|
|
b4a9da92f6 | ||
|
|
07101a1b84 | ||
|
|
e0af19583c | ||
|
|
36261a6211 | ||
|
|
ad0a5c7932 | ||
|
|
65149c33da | ||
|
|
3d7b3c703a | ||
|
|
b446d2d1ac | ||
|
|
43115d1da4 | ||
|
|
484f21fac4 | ||
|
|
704109daa4 | ||
|
|
48475fb0eb | ||
|
|
eab55bc7dd | ||
|
|
61847d94f5 | ||
|
|
2f1eabcdc9 | ||
|
|
e7ba8e7739 | ||
|
|
b1e39ab4b3 | ||
|
|
14075c6b9b | ||
|
|
72c1acdfd2 | ||
|
|
ccc03c8ae0 | ||
|
|
9be085dd90 | ||
|
|
9483794d20 | ||
|
|
1af35bff7f | ||
|
|
23068d2819 | ||
|
|
f00dfa95fd | ||
|
|
a9b8b4193c | ||
|
|
3b6148c841 | ||
|
|
9a0f06d195 | ||
|
|
3e8755621a | ||
|
|
c4047ff57a | ||
|
|
558b9e7c30 | ||
|
|
f49850ae8a | ||
|
|
0e129ab00f | ||
|
|
708944d078 | ||
|
|
5291bbd596 | ||
|
|
c0a1dc6056 | ||
|
|
34b7ba97b7 | ||
|
|
3c29165028 | ||
|
|
1b523c94f5 | ||
|
|
0d9e28f8d1 | ||
|
|
d2c92eedd3 | ||
|
|
8d09bfb1a9 | ||
|
|
bf52f02b82 | ||
|
|
d247dbebfc | ||
|
|
e68c4e134d | ||
|
|
ac734ecf71 | ||
|
|
32822703e9 | ||
|
|
b16d0186a1 | ||
|
|
04706eb9de | ||
|
|
a481ca3c89 | ||
|
|
a061d440d7 | ||
|
|
db84e6d318 | ||
|
|
79c7074b8e | ||
|
|
ecdd5ae88d | ||
|
|
0de454fc25 | ||
|
|
a007434fc5 | ||
|
|
e16a12e401 | ||
|
|
8edc9673d8 | ||
|
|
c494907b19 | ||
|
|
90b7373af2 | ||
|
|
9f0a18ecce | ||
|
|
1f2eee133b | ||
|
|
a111131a16 | ||
|
|
46486d75c9 | ||
|
|
f55ea7d320 | ||
|
|
09815f9614 | ||
|
|
91395d4b31 | ||
|
|
807a789c83 | ||
|
|
7ee832d71f | ||
|
|
1957191d07 | ||
|
|
927b438a4b | ||
|
|
c420f0397c | ||
|
|
c516f8d1d9 | ||
|
|
d9f6aad7d4 | ||
|
|
037b01e08c | ||
|
|
2fec36d41b | ||
|
|
0ea63a78b4 | ||
|
|
582d159332 | ||
|
|
35cf36f8c0 | ||
|
|
a22e92b1b5 | ||
|
|
15b219a773 | ||
|
|
571992106d | ||
|
|
78106b0ad3 | ||
|
|
21418954c1 | ||
|
|
bad68830ff | ||
|
|
d3bb076b71 | ||
|
|
a750aedeb8 | ||
|
|
27247aec12 | ||
|
|
ddcf370ffa | ||
|
|
d681ed762d | ||
|
|
9165b3abb7 | ||
|
|
41d3b4fbba | ||
|
|
fc0c34d974 | ||
|
|
a1e8a37147 |
7
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
7
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
@ -9,9 +9,10 @@ body:
|
|||||||
description: 在提交之前,请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
|
description: 在提交之前,请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
|
||||||
options:
|
options:
|
||||||
- label: 我已在标题简短的描述了我所遇到的问题
|
- label: 我已在标题简短的描述了我所遇到的问题
|
||||||
- label: 我未在[Issue Tracker](./?q=is%3Aissue)中找到我要提出的问题
|
- label: 我已在 [Issue Tracker](./?q=is%3Aissue) 中寻找过我要提出的问题,但未找到相同的问题
|
||||||
- label: 我未在[常见问题](https://mihomo.party/docs/issues/common)中找到我要提出的问题
|
- label: 我已在 [常见问题](https://mihomo.party/docs/issues/common) 中寻找过我要提出的问题,并没有找到答案
|
||||||
- label: 这是GUI程序的问题,而不是内核程序的问题
|
- label: 这是 GUI 程序的问题,而不是内核程序的问题
|
||||||
|
- label: 我已经关闭所有杀毒软件/代理软件后测试过,问题依旧存在
|
||||||
- label: 我已经使用最新的测试版本测试过,问题依旧存在
|
- label: 我已经使用最新的测试版本测试过,问题依旧存在
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -3,3 +3,6 @@ contact_links:
|
|||||||
- name: '常见问题'
|
- name: '常见问题'
|
||||||
about: '提出问题前请先查看常见问题'
|
about: '提出问题前请先查看常见问题'
|
||||||
url: 'https://mihomo.party/docs/issues/common'
|
url: 'https://mihomo.party/docs/issues/common'
|
||||||
|
- name: '交流群组'
|
||||||
|
about: '提问/讨论性质的问题请勿提交issue'
|
||||||
|
url: 'https://t.me/mihomo_party_group'
|
||||||
|
|||||||
@ -9,8 +9,8 @@ body:
|
|||||||
description: 在提交之前,请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
|
description: 在提交之前,请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
|
||||||
options:
|
options:
|
||||||
- label: 我已在标题简短的描述了我所需的功能
|
- label: 我已在标题简短的描述了我所需的功能
|
||||||
- label: 我已在[Issue Tracker](./?q=is%3Aissue)中寻找过,但未找到我所需的功能
|
- label: 我已在 [Issue Tracker](./?q=is%3Aissue) 中寻找过,但未找到我所需的功能
|
||||||
- label: 这是向GUI程序提出的功能请求,而不是内核程序
|
- label: 这是向 GUI 程序提出的功能请求,而不是内核程序
|
||||||
- label: 我未在最新的测试版本找到我所需的功能
|
- label: 我未在最新的测试版本找到我所需的功能
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
147
.github/workflows/build.yml
vendored
147
.github/workflows/build.yml
vendored
@ -1,10 +1,13 @@
|
|||||||
name: Build
|
name: Build
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
paths-ignore:
|
||||||
|
- 'README.md'
|
||||||
|
- '.github/ISSUE_TEMPLATE/**'
|
||||||
|
- '.github/workflows/issues.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
|
|
||||||
@ -142,6 +145,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm add @mihomo-party/sysproxy-linux-${{ matrix.arch }}-gnu
|
pnpm add @mihomo-party/sysproxy-linux-${{ matrix.arch }}-gnu
|
||||||
|
sed -i "s/productName: Mihomo Party/productName: mihomo-party/" electron-builder.yml
|
||||||
pnpm prepare --${{ matrix.arch }}
|
pnpm prepare --${{ matrix.arch }}
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
@ -196,9 +200,35 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
npm_config_target_arch: ${{ matrix.arch }}
|
npm_config_target_arch: ${{ matrix.arch }}
|
||||||
run: pnpm build:mac --${{ matrix.arch }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
|
||||||
|
pnpm build:mac --${{ matrix.arch }}
|
||||||
|
- name: Setup temporary installer signing keychain
|
||||||
|
uses: apple-actions/import-codesign-certs@v3
|
||||||
|
with:
|
||||||
|
p12-file-base64: ${{ secrets.CSC_INSTALLER_LINK }}
|
||||||
|
p12-password: ${{ secrets.CSC_INSTALLER_KEY_PASSWORD }}
|
||||||
|
- name: Sign the Apple pkg
|
||||||
|
run: |
|
||||||
|
for pkg_name in $(ls -1 dist/*.pkg); do
|
||||||
|
pkg_name=$(ls -1 dist/*.pkg)
|
||||||
|
mv $pkg_name Unsigned-Workbench.pkg
|
||||||
|
productsign --sign "Developer ID Installer: Prometheus Advertising Corp (489PDK5LP3)" Unsigned-Workbench.pkg $pkg_name
|
||||||
|
rm -f Unsigned-Workbench.pkg
|
||||||
|
xcrun notarytool submit $pkg_name --apple-id $APPLE_ID --team-id $APPLE_TEAM_ID --password $APPLE_APP_SPECIFIC_PASSWORD --wait
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
- name: Generate checksums
|
- name: Generate checksums
|
||||||
run: pnpm checksum .dmg
|
run: pnpm checksum .pkg
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
if: startsWith(github.ref, 'refs/heads/')
|
if: startsWith(github.ref, 'refs/heads/')
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@ -206,7 +236,7 @@ jobs:
|
|||||||
name: MacOS ${{ matrix.arch }}
|
name: MacOS ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
dist/*.sha256
|
dist/*.sha256
|
||||||
dist/*.dmg
|
dist/*.pkg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
- name: Publish Release
|
- name: Publish Release
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
@ -214,25 +244,102 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/*.sha256
|
dist/*.sha256
|
||||||
dist/*.dmg
|
dist/*.pkg
|
||||||
|
body_path: changelog.md
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
macos10:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
- arm64
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup pnpm
|
||||||
|
run: npm install -g pnpm
|
||||||
|
- name: Install Dependencies
|
||||||
|
env:
|
||||||
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
|
npm_config_target_arch: ${{ matrix.arch }}
|
||||||
|
run: |
|
||||||
|
pnpm install
|
||||||
|
pnpm add @mihomo-party/sysproxy-darwin-${{ matrix.arch }}
|
||||||
|
pnpm add -D electron@32.2.2
|
||||||
|
pnpm prepare --${{ matrix.arch }}
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
|
npm_config_target_arch: ${{ matrix.arch }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
sed -i "" -e "s/macos/catalina/" electron-builder.yml
|
||||||
|
chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
|
||||||
|
pnpm build:mac --${{ matrix.arch }}
|
||||||
|
- name: Setup temporary installer signing keychain
|
||||||
|
uses: apple-actions/import-codesign-certs@v3
|
||||||
|
with:
|
||||||
|
p12-file-base64: ${{ secrets.CSC_INSTALLER_LINK }}
|
||||||
|
p12-password: ${{ secrets.CSC_INSTALLER_KEY_PASSWORD }}
|
||||||
|
- name: Sign the Apple pkg
|
||||||
|
run: |
|
||||||
|
for pkg_name in $(ls -1 dist/*.pkg); do
|
||||||
|
pkg_name=$(ls -1 dist/*.pkg)
|
||||||
|
mv $pkg_name Unsigned-Workbench.pkg
|
||||||
|
productsign --sign "Developer ID Installer: Prometheus Advertising Corp (489PDK5LP3)" Unsigned-Workbench.pkg $pkg_name
|
||||||
|
rm -f Unsigned-Workbench.pkg
|
||||||
|
xcrun notarytool submit $pkg_name --apple-id $APPLE_ID --team-id $APPLE_TEAM_ID --password $APPLE_APP_SPECIFIC_PASSWORD --wait
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
- name: Generate checksums
|
||||||
|
run: pnpm checksum .pkg
|
||||||
|
- name: Upload Artifacts
|
||||||
|
if: startsWith(github.ref, 'refs/heads/')
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Catalina ${{ matrix.arch }}
|
||||||
|
path: |
|
||||||
|
dist/*.sha256
|
||||||
|
dist/*.pkg
|
||||||
|
if-no-files-found: error
|
||||||
|
- name: Publish Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
dist/*.sha256
|
||||||
|
dist/*.pkg
|
||||||
body_path: changelog.md
|
body_path: changelog.md
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
updater:
|
updater:
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: [windows, macos, windows7]
|
needs: [windows, macos, windows7, macos10]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
run: npm install -g pnpm
|
run: npm install -g pnpm
|
||||||
- name: Build Latest
|
- name: Install Dependencies
|
||||||
run: pnpm install && pnpm updater
|
run: pnpm install
|
||||||
- name: Telegram Notification
|
- name: Telegram Notification
|
||||||
env:
|
env:
|
||||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
run: pnpm telegram
|
run: pnpm telegram
|
||||||
|
- name: Generate latest.yml
|
||||||
|
run: pnpm updater
|
||||||
- name: Publish Release
|
- name: Publish Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
@ -280,7 +387,7 @@ jobs:
|
|||||||
pkgbuild: aur/${{ matrix.pkgname }}/PKGBUILD
|
pkgbuild: aur/${{ matrix.pkgname }}/PKGBUILD
|
||||||
commit_username: pompurin404
|
commit_username: pompurin404
|
||||||
commit_email: pompurin404@mihomo.party
|
commit_email: pompurin404@mihomo.party
|
||||||
ssh_private_key: ${{ secrets.PRIVATE_KEY }}
|
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
commit_message: Update AUR package
|
commit_message: Update AUR package
|
||||||
ssh_keyscan_types: rsa,ed25519
|
ssh_keyscan_types: rsa,ed25519
|
||||||
allow_empty_commits: false
|
allow_empty_commits: false
|
||||||
@ -302,7 +409,7 @@ jobs:
|
|||||||
pkgbuild: aur/mihomo-party-git/PKGBUILD
|
pkgbuild: aur/mihomo-party-git/PKGBUILD
|
||||||
commit_username: pompurin404
|
commit_username: pompurin404
|
||||||
commit_email: pompurin404@mihomo.party
|
commit_email: pompurin404@mihomo.party
|
||||||
ssh_private_key: ${{ secrets.PRIVATE_KEY }}
|
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
commit_message: Update AUR package
|
commit_message: Update AUR package
|
||||||
ssh_keyscan_types: rsa,ed25519
|
ssh_keyscan_types: rsa,ed25519
|
||||||
allow_empty_commits: false
|
allow_empty_commits: false
|
||||||
@ -323,21 +430,3 @@ jobs:
|
|||||||
release-tag: v${{env.VERSION}}
|
release-tag: v${{env.VERSION}}
|
||||||
installers-regex: 'mihomo-party-windows-.*setup\.exe$'
|
installers-regex: 'mihomo-party-windows-.*setup\.exe$'
|
||||||
token: ${{ secrets.POMPURIN404_TOKEN }}
|
token: ${{ secrets.POMPURIN404_TOKEN }}
|
||||||
|
|
||||||
homebrew:
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
name: Update Homebrew cask
|
|
||||||
needs: macos
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Set up Git
|
|
||||||
run: |
|
|
||||||
git config --global user.email pompurin404@mihomo.party
|
|
||||||
git config --global user.name pompurin404
|
|
||||||
- name: Update Homebrew cask
|
|
||||||
env:
|
|
||||||
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.POMPURIN404_TOKEN }}
|
|
||||||
run: |
|
|
||||||
brew tap mihomo-party-org/mihomo-party
|
|
||||||
brew update
|
|
||||||
brew bump-cask-pr mihomo-party --version $(echo ${{ github.ref }} | tr -d 'refs/tags/v') --no-browse
|
|
||||||
|
|||||||
29
.github/workflows/issues.yml
vendored
Normal file
29
.github/workflows/issues.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: Review Issues
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Generate Token
|
||||||
|
uses: tibdex/github-app-token@v2
|
||||||
|
id: generate
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.BOT_APP_ID }}
|
||||||
|
private_key: ${{ secrets.BOT_PRIVATE_KEY }}
|
||||||
|
- name: Review Issues
|
||||||
|
uses: mihomo-party-org/universal-assistant@v1.0.3
|
||||||
|
with:
|
||||||
|
github_token: ${{ steps.generate.outputs.token }}
|
||||||
|
openai_base_url: ${{ secrets.OPENAI_BASE_URL }}
|
||||||
|
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
openai_model: ${{ vars.OPENAI_MODEL }}
|
||||||
|
system_prompt: ${{ vars.SYSTEM_PROMPT }}
|
||||||
|
available_tools: ${{ vars.AVAILABLE_TOOLS }}
|
||||||
|
user_input: |
|
||||||
|
请审查如下 Issue:
|
||||||
|
标题:"${{ github.event.issue.title }}"
|
||||||
|
内容:"${{ github.event.issue.body }}"
|
||||||
2
.npmrc
2
.npmrc
@ -1,3 +1,3 @@
|
|||||||
shamefully-hoist=true
|
shamefully-hoist=true
|
||||||
virtual-store-dir-max-length=80
|
virtual-store-dir-max-length=80
|
||||||
public-hoist-pattern[]=*@nextui-org/*
|
public-hoist-pattern[]=*@heroui/*
|
||||||
19
README.md
19
README.md
@ -3,27 +3,38 @@
|
|||||||
<img height='48px' src='./images/icon-black.png#gh-light-mode-only'>
|
<img height='48px' src='./images/icon-black.png#gh-light-mode-only'>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<h3 align="center">Another Mihomo GUI</h3>
|
<h3 align="center">Another <a href="https://github.com/MetaCubeX/mihomo">Mihomo</a> GUI</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/mihomo-party-org/mihomo-party/releases">
|
<a href="https://github.com/mihomo-party-org/mihomo-party/releases">
|
||||||
<img src="https://img.shields.io/github/release/mihomo-party-org/mihomo-party/all.svg">
|
<img src="https://img.shields.io/github/release/mihomo-party-org/mihomo-party/all.svg">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://t.me/mihomo_party_channel">
|
<a href="https://t.me/mihomo_party_group">
|
||||||
<img src="https://img.shields.io/badge/Telegram-Channel-blue?logo=telegram">
|
<img src="https://img.shields.io/badge/Telegram-Group-blue?logo=telegram">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<div align='center'>
|
<div align='center'>
|
||||||
<img width='90%' src="./images/preview.jpg">
|
<img width='90%' src="./images/preview.jpg">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
### 本项目由“[狗狗加速](https://party.dginv.click/#/register?code=ARdo0mXx)”赞助
|
||||||
|
##### [狗狗加速 —— 技术流机场 Doggygo VPN](https://party.dginv.click/#/register?code=ARdo0mXx)
|
||||||
|
|
||||||
|
- 高性能海外机场,稳定首选,海外团队,无跑路风险
|
||||||
|
- Mihomo Party专属8折优惠码:party,仅有500份
|
||||||
|
- Party专属链接注册送 3 天,每天 1G 流量 [免费试用](https://party.dginv.click/#/register?code=ARdo0mXx)
|
||||||
|
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
||||||
|
- 全球首家支持Hysteria1/2 协议,集群负载均衡设计,高速专线,基于最新UDP quic技术,极低延迟,无视晚高峰,4K 秒开,配合Mihomo Party食用更省心!
|
||||||
|
- 解锁流媒体及 ChatGPT
|
||||||
|
- 官网:[https://狗狗加速.com](https://party.dginv.click/#/register?code=ARdo0mXx)
|
||||||
|
|
||||||
### 特性
|
### 特性
|
||||||
|
|
||||||
- [x] 开箱即用,无需服务模式的 Tun
|
- [x] 开箱即用,无需服务模式的 Tun
|
||||||
- [x] 多种配色主题可选,UI 焕然一新
|
- [x] 多种配色主题可选,UI 焕然一新
|
||||||
- [x] 支持大部分 Mihomo 常用配置修改
|
- [x] 支持大部分 Mihomo 常用配置修改
|
||||||
- [x] 内置稳定版和预览版 Mihomo 内核
|
- [x] 内置稳定版和预览版 Mihomo 内核
|
||||||
- [x] 通过WebDav一键备份和恢复配置
|
- [x] 通过 WebDAV 一键备份和恢复配置
|
||||||
- [x] 强大的覆写功能,任意修订配置文件
|
- [x] 强大的覆写功能,任意修订配置文件
|
||||||
- [x] 深度集成 Sub-Store,轻松管理订阅
|
- [x] 深度集成 Sub-Store,轻松管理订阅
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,6 @@ conflicts=("$_pkgname" "$_pkgname-git" "$_pkgname-electron" "$_pkgname-electron-
|
|||||||
conflicts=("mihomo-party-git" 'mihomo-party')
|
conflicts=("mihomo-party-git" 'mihomo-party')
|
||||||
depends=('gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
|
depends=('gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'util-linux-libs' 'libsecret')
|
||||||
optdepends=('libappindicator-gtk3: Allow mihomo-party to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
|
optdepends=('libappindicator-gtk3: Allow mihomo-party to extend a menu via Ayatana indicators in Unity, KDE or Systray (GTK+ 3 library).')
|
||||||
makedepends=('nodejs' 'pnpm' 'jq' 'libxcrypt-compat')
|
|
||||||
install=$_pkgname.install
|
install=$_pkgname.install
|
||||||
source=("${_pkgname}.sh")
|
source=("${_pkgname}.sh")
|
||||||
source_x86_64=("${_pkgname}-${pkgver}-x86_64.deb::${url}/releases/download/v${pkgver}/mihomo-party-linux-${pkgver}-amd64.deb")
|
source_x86_64=("${_pkgname}-${pkgver}-x86_64.deb::${url}/releases/download/v${pkgver}/mihomo-party-linux-${pkgver}-amd64.deb")
|
||||||
@ -22,10 +21,9 @@ sha256sums_aarch64=('8cd7398b8fc1cd70d41e386af9995cbddc1043d9018391c29f056f14357
|
|||||||
package() {
|
package() {
|
||||||
bsdtar -xf data.tar.xz -C "${pkgdir}/"
|
bsdtar -xf data.tar.xz -C "${pkgdir}/"
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/mihomo-party
|
chmod +x ${pkgdir}/opt/mihomo-party/mihomo-party
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
|
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
|
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
|
||||||
cd ${pkgdir}/../..
|
install -Dm755 "${srcdir}/${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
|
||||||
install -Dm755 "${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
|
|
||||||
sed -i '3s!/opt/mihomo-party/mihomo-party!mihomo-party!' "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
|
sed -i '3s!/opt/mihomo-party/mihomo-party!mihomo-party!' "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
|
||||||
|
|
||||||
chown -R root:root ${pkgdir}
|
chown -R root:root ${pkgdir}
|
||||||
|
|||||||
@ -27,10 +27,9 @@ package() {
|
|||||||
asar extract $srcdir/opt/mihomo-party/resources/app.asar ${pkgdir}/opt/mihomo-party
|
asar extract $srcdir/opt/mihomo-party/resources/app.asar ${pkgdir}/opt/mihomo-party
|
||||||
cp -r $srcdir/opt/mihomo-party/resources/sidecar ${pkgdir}/opt/mihomo-party/resources/
|
cp -r $srcdir/opt/mihomo-party/resources/sidecar ${pkgdir}/opt/mihomo-party/resources/
|
||||||
cp -r $srcdir/opt/mihomo-party/resources/files ${pkgdir}/opt/mihomo-party/resources/
|
cp -r $srcdir/opt/mihomo-party/resources/files ${pkgdir}/opt/mihomo-party/resources/
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
|
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
|
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
|
||||||
cd ${pkgdir}/../..
|
install -Dm755 "${srcdir}/${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
|
||||||
install -Dm755 "${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
|
|
||||||
install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
|
install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
|
||||||
install -Dm644 "${pkgdir}/opt/mihomo-party/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"
|
install -Dm644 "${pkgdir}/opt/mihomo-party/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=mihomo-party
|
Name=Mihomo Party
|
||||||
Exec=mihomo-party %U
|
Exec=mihomo-party %U
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
|
|||||||
@ -24,6 +24,7 @@ options=('!lto')
|
|||||||
|
|
||||||
prepare(){
|
prepare(){
|
||||||
cd $srcdir/${_pkgname}-${pkgver}
|
cd $srcdir/${_pkgname}-${pkgver}
|
||||||
|
sed -i "s/productName: Mihomo Party/productName: mihomo-party/" electron-builder.yml
|
||||||
pnpm install
|
pnpm install
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,9 +37,8 @@ package() {
|
|||||||
asar extract $srcdir/${_pkgname}-${pkgver}/dist/linux-unpacked/resources/app.asar ${pkgdir}/opt/mihomo-party
|
asar extract $srcdir/${_pkgname}-${pkgver}/dist/linux-unpacked/resources/app.asar ${pkgdir}/opt/mihomo-party
|
||||||
cp -r $srcdir/${_pkgname}-${pkgver}/extra/sidecar ${pkgdir}/opt/mihomo-party/resources/
|
cp -r $srcdir/${_pkgname}-${pkgver}/extra/sidecar ${pkgdir}/opt/mihomo-party/resources/
|
||||||
cp -r $srcdir/${_pkgname}-${pkgver}/extra/files ${pkgdir}/opt/mihomo-party/resources/
|
cp -r $srcdir/${_pkgname}-${pkgver}/extra/files ${pkgdir}/opt/mihomo-party/resources/
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
|
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
|
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
|
||||||
cd ${pkgdir}/../..
|
|
||||||
install -Dm755 "${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
|
install -Dm755 "${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
|
||||||
install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
|
install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
|
||||||
install -Dm644 "${pkgdir}/opt/mihomo-party/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"
|
install -Dm644 "${pkgdir}/opt/mihomo-party/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=mihomo-party
|
Name=Mihomo Party
|
||||||
Exec=mihomo-party %U
|
Exec=mihomo-party %U
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
|
|||||||
@ -25,6 +25,7 @@ pkgver() {
|
|||||||
|
|
||||||
prepare(){
|
prepare(){
|
||||||
cd $srcdir/${_pkgname}
|
cd $srcdir/${_pkgname}
|
||||||
|
sed -i "s/productName: Mihomo Party/productName: mihomo-party/" electron-builder.yml
|
||||||
pnpm install
|
pnpm install
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,11 +39,9 @@ package() {
|
|||||||
bsdtar -xf mihomo-party-linux-$(jq '.version' $srcdir/${_pkgname}/package.json | tr -d 'v"')*.deb
|
bsdtar -xf mihomo-party-linux-$(jq '.version' $srcdir/${_pkgname}/package.json | tr -d 'v"')*.deb
|
||||||
bsdtar -xf data.tar.xz -C "${pkgdir}/"
|
bsdtar -xf data.tar.xz -C "${pkgdir}/"
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/mihomo-party
|
chmod +x ${pkgdir}/opt/mihomo-party/mihomo-party
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
|
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
|
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
|
||||||
cd ${pkgdir}/../..
|
install -Dm755 "${srcdir}/../${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
|
||||||
install -Dm755 "${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
|
|
||||||
|
|
||||||
sed -i '3s!/opt/mihomo-party/mihomo-party!mihomo-party!' "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
|
sed -i '3s!/opt/mihomo-party/mihomo-party!mihomo-party!' "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
|
||||||
|
|
||||||
chown -R root:root ${pkgdir}
|
chown -R root:root ${pkgdir}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ options=('!lto')
|
|||||||
|
|
||||||
prepare(){
|
prepare(){
|
||||||
cd $srcdir/${pkgname}-${pkgver}
|
cd $srcdir/${pkgname}-${pkgver}
|
||||||
|
sed -i "s/productName: Mihomo Party/productName: mihomo-party/" electron-builder.yml
|
||||||
pnpm install
|
pnpm install
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,10 +34,9 @@ package() {
|
|||||||
bsdtar -xf mihomo-party-linux-${pkgver}*.deb
|
bsdtar -xf mihomo-party-linux-${pkgver}*.deb
|
||||||
bsdtar -xf data.tar.xz -C "${pkgdir}/"
|
bsdtar -xf data.tar.xz -C "${pkgdir}/"
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/mihomo-party
|
chmod +x ${pkgdir}/opt/mihomo-party/mihomo-party
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
|
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
|
||||||
chmod +x ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
|
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
|
||||||
cd ${pkgdir}/../..
|
install -Dm755 "${srcdir}/../${pkgname}.sh" "${pkgdir}/usr/bin/${pkgname}"
|
||||||
install -Dm755 "${pkgname}.sh" "${pkgdir}/usr/bin/${pkgname}"
|
|
||||||
sed -i '3s!/opt/mihomo-party/mihomo-party!mihomo-party!' "${pkgdir}/usr/share/applications/${pkgname}.desktop"
|
sed -i '3s!/opt/mihomo-party/mihomo-party!mihomo-party!' "${pkgdir}/usr/share/applications/${pkgname}.desktop"
|
||||||
|
|
||||||
chown -R root:root ${pkgdir}
|
chown -R root:root ${pkgdir}
|
||||||
|
|||||||
BIN
build/background.png
Normal file
BIN
build/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@ -11,6 +11,8 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
chmod 4755 '/opt/mihomo-party/chrome-sandbox' || true
|
chmod 4755 '/opt/mihomo-party/chrome-sandbox' || true
|
||||||
|
chmod +sx /opt/mihomo-party/resources/sidecar/mihomo
|
||||||
|
chmod +sx /opt/mihomo-party/resources/sidecar/mihomo-alpha
|
||||||
|
|
||||||
if hash update-mime-database 2>/dev/null; then
|
if hash update-mime-database 2>/dev/null; then
|
||||||
update-mime-database /usr/share/mime || true
|
update-mime-database /usr/share/mime || true
|
||||||
173
build/pkg-scripts/postinstall
Normal file
173
build/pkg-scripts/postinstall
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 设置日志文件
|
||||||
|
LOG_FILE="/tmp/mihomo-party-install.log"
|
||||||
|
exec > "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查 root 权限
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
log "Error: Please run as root"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 判断 $2 是否以 .app 结尾
|
||||||
|
if [[ $2 == *".app" ]]; then
|
||||||
|
APP_PATH="$2"
|
||||||
|
else
|
||||||
|
APP_PATH="$2/Mihomo Party.app"
|
||||||
|
fi
|
||||||
|
|
||||||
|
HELPER_PATH="/Library/PrivilegedHelperTools/party.mihomo.helper"
|
||||||
|
LAUNCH_DAEMON="/Library/LaunchDaemons/party.mihomo.helper.plist"
|
||||||
|
|
||||||
|
log "Starting installation..."
|
||||||
|
|
||||||
|
# 创建目录并设置权限
|
||||||
|
log "Creating directories and setting permissions..."
|
||||||
|
mkdir -p "/Library/PrivilegedHelperTools"
|
||||||
|
chmod 755 "/Library/PrivilegedHelperTools"
|
||||||
|
chown root:wheel "/Library/PrivilegedHelperTools"
|
||||||
|
|
||||||
|
# 设置核心文件权限
|
||||||
|
log "Setting core file permissions..."
|
||||||
|
if [ -f "$APP_PATH/Contents/Resources/sidecar/mihomo" ]; then
|
||||||
|
chown root:admin "$APP_PATH/Contents/Resources/sidecar/mihomo"
|
||||||
|
chmod +s "$APP_PATH/Contents/Resources/sidecar/mihomo"
|
||||||
|
log "Set permissions for mihomo"
|
||||||
|
else
|
||||||
|
log "Warning: mihomo binary not found at $APP_PATH/Contents/Resources/sidecar/mihomo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$APP_PATH/Contents/Resources/sidecar/mihomo-alpha" ]; then
|
||||||
|
chown root:admin "$APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
|
||||||
|
chmod +s "$APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
|
||||||
|
log "Set permissions for mihomo-alpha"
|
||||||
|
else
|
||||||
|
log "Warning: mihomo-alpha binary not found at $APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 复制 helper 工具
|
||||||
|
log "Installing helper tool..."
|
||||||
|
if [ -f "$APP_PATH/Contents/Resources/files/party.mihomo.helper" ]; then
|
||||||
|
cp -f "$APP_PATH/Contents/Resources/files/party.mihomo.helper" "$HELPER_PATH"
|
||||||
|
chown root:wheel "$HELPER_PATH"
|
||||||
|
chmod 544 "$HELPER_PATH"
|
||||||
|
log "Helper tool installed successfully"
|
||||||
|
else
|
||||||
|
log "Error: Helper file not found at $APP_PATH/Contents/Resources/files/party.mihomo.helper"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建并配置 LaunchDaemon
|
||||||
|
log "Configuring LaunchDaemon..."
|
||||||
|
mkdir -p "/Library/LaunchDaemons"
|
||||||
|
cat << EOF > "$LAUNCH_DAEMON"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>party.mihomo.helper</string>
|
||||||
|
<key>AssociatedBundleIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>party.mihomo.app</string>
|
||||||
|
</array>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>Program</key>
|
||||||
|
<string>${HELPER_PATH}</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/tmp/party.mihomo.helper.err</string>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/tmp/party.mihomo.helper.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:wheel "$LAUNCH_DAEMON"
|
||||||
|
chmod 644 "$LAUNCH_DAEMON"
|
||||||
|
log "LaunchDaemon configured"
|
||||||
|
|
||||||
|
# 验证关键文件
|
||||||
|
log "Verifying installation..."
|
||||||
|
if [ ! -x "$HELPER_PATH" ]; then
|
||||||
|
log "Error: Helper tool is not executable: $HELPER_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查二进制文件有效性
|
||||||
|
if ! file "$HELPER_PATH" | grep -q "Mach-O"; then
|
||||||
|
log "Error: Helper tool is not a valid Mach-O binary"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 验证 plist 格式
|
||||||
|
if ! plutil -lint "$LAUNCH_DAEMON" >/dev/null 2>&1; then
|
||||||
|
log "Error: Invalid plist format"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 获取 macOS 版本
|
||||||
|
macos_version=$(sw_vers -productVersion)
|
||||||
|
macos_major=$(echo "$macos_version" | cut -d. -f1)
|
||||||
|
log "macOS version: $macos_version"
|
||||||
|
|
||||||
|
# 清理现有服务
|
||||||
|
log "Cleaning up existing services..."
|
||||||
|
launchctl bootout system "$LAUNCH_DAEMON" 2>/dev/null || true
|
||||||
|
launchctl unload "$LAUNCH_DAEMON" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 加载服务
|
||||||
|
log "Loading service..."
|
||||||
|
if [ "$macos_major" -ge 11 ]; then
|
||||||
|
# macOS Big Sur 及更新版本使用 bootstrap
|
||||||
|
if ! launchctl bootstrap system "$LAUNCH_DAEMON"; then
|
||||||
|
log "Bootstrap failed, trying legacy load..."
|
||||||
|
if ! launchctl load "$LAUNCH_DAEMON"; then
|
||||||
|
log "Error: Failed to load service with both methods"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# 旧版本使用 load
|
||||||
|
if ! launchctl load "$LAUNCH_DAEMON"; then
|
||||||
|
log "Error: Failed to load service"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 验证服务状态
|
||||||
|
log "Verifying service status..."
|
||||||
|
sleep 2
|
||||||
|
if launchctl list | grep -q "party.mihomo.helper"; then
|
||||||
|
log "Service loaded successfully"
|
||||||
|
else
|
||||||
|
log "Warning: Service may not be running properly"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Installation completed successfully"
|
||||||
|
|
||||||
|
# Fix user data directory permissions
|
||||||
|
log "Fixing user data directory permissions..."
|
||||||
|
for user_home in /Users/*; do
|
||||||
|
if [ -d "$user_home" ] && [ "$(basename "$user_home")" != "Shared" ] && [ "$(basename "$user_home")" != ".localized" ]; then
|
||||||
|
username=$(basename "$user_home")
|
||||||
|
user_data_dir="$user_home/Library/Application Support/mihomo-party"
|
||||||
|
|
||||||
|
if [ -d "$user_data_dir" ]; then
|
||||||
|
current_owner=$(stat -f "%Su" "$user_data_dir" 2>/dev/null || echo "unknown")
|
||||||
|
if [ "$current_owner" = "root" ]; then
|
||||||
|
log "Fixing ownership for user: $username"
|
||||||
|
chown -R "$username:staff" "$user_data_dir" 2>/dev/null || true
|
||||||
|
chmod -R u+rwX "$user_data_dir" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 0
|
||||||
26
build/pkg-scripts/preinstall
Normal file
26
build/pkg-scripts/preinstall
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 检查 root 权限
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "Please run as root"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HELPER_PATH="/Library/PrivilegedHelperTools/party.mihomo.helper"
|
||||||
|
LAUNCH_DAEMON="/Library/LaunchDaemons/party.mihomo.helper.plist"
|
||||||
|
|
||||||
|
# 停止并卸载现有的 LaunchDaemon
|
||||||
|
if [ -f "$LAUNCH_DAEMON" ]; then
|
||||||
|
launchctl unload "$LAUNCH_DAEMON" 2>/dev/null || true
|
||||||
|
rm -f "$LAUNCH_DAEMON"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 移除 helper 工具
|
||||||
|
rm -f "$HELPER_PATH"
|
||||||
|
|
||||||
|
# 清理可能存在的旧版本文件
|
||||||
|
rm -rf "/Applications/Mihomo Party.app"
|
||||||
|
rm -rf "/Applications/Mihomo\\ Party.app"
|
||||||
|
|
||||||
|
exit 0
|
||||||
76
changelog.md
76
changelog.md
@ -1,14 +1,72 @@
|
|||||||
### Breaking Changes
|
## 1.7.7
|
||||||
|
### 新功能 (Feat)
|
||||||
|
- Mihomo 内核升级 v1.19.12
|
||||||
|
- 新增 Webdav 最大备数设置和清理逻辑
|
||||||
|
|
||||||
- 为了修复macOS应用内更新问题,此版本需要手动下载dmg进行安装
|
### 修复 (Fix)
|
||||||
|
- 修复 MacOS 下无法启动的问题(重置工作目录权限)
|
||||||
|
- 尝试修复不同版本 MacOS 下安装软件时候的报错(Input/output error)
|
||||||
|
- 部分遗漏的多国语言翻译
|
||||||
|
|
||||||
### Features
|
## 1.7.6
|
||||||
|
|
||||||
- 支持自定义延迟测试并发数量
|
**此版本修复了 1.7.5 中的几个严重 bug,推荐所有人更新**
|
||||||
- 完善Sub-Store环境变量
|
|
||||||
- 支持查看已关闭的连接
|
|
||||||
|
|
||||||
### Bug Fixes
|
### 修复 (Fix)
|
||||||
|
- 修复了内核1.19.8更新后gist同步失效的问题(#780)
|
||||||
|
- 部分遗漏的多国语言翻译
|
||||||
|
- MacOS 下启动Error: EACCES: permission denied
|
||||||
|
- MacOS 系统代理 bypass 不生效
|
||||||
|
- MacOS 系统代理开启时 500 报错
|
||||||
|
|
||||||
- 修复macOS应用内更新后权限丢失的问题
|
## 1.7.5
|
||||||
- 修复高版本macOS SSID获取失败的问题
|
|
||||||
|
### 新功能 (Feat)
|
||||||
|
- 增加组延迟测试时的动画
|
||||||
|
- 订阅卡片可右键点击
|
||||||
|
-
|
||||||
|
|
||||||
|
### 修复 (Fix)
|
||||||
|
- 1.7.4引入的内核启动错误
|
||||||
|
- 无法手动设置内核权限
|
||||||
|
- 完善 系统代理socket 重建和检测机制
|
||||||
|
|
||||||
|
## 1.7.4
|
||||||
|
|
||||||
|
### 新功能 (Feat)
|
||||||
|
- Mihomo 内核升级 v1.19.10
|
||||||
|
- 改进 socket创建机制,防止 MacOS 下系统代理开启无法找到 socket 文件的问题
|
||||||
|
- mihomo-party-helper增加更多日志,以方便调试
|
||||||
|
- 改进 MacOS 下签名和公正流程
|
||||||
|
- 增加 MacOS 下 plist 权限设置
|
||||||
|
- 改进安装流程
|
||||||
|
-
|
||||||
|
|
||||||
|
### 修复 (Fix)
|
||||||
|
- 修复mihomo-party-helper本地提权漏洞
|
||||||
|
- 修复 MacOS 下安装失败的问题
|
||||||
|
- 移除节点页面的滚动位置记忆,解决页面溢出的问题
|
||||||
|
- DNS hosts 设置在 useHosts 不为 true 时也会被错误应用的问题(#742)
|
||||||
|
- 当用户在 Profile 设置中修改了更新间隔并保存后,新的间隔时间不会立即生效(#671)
|
||||||
|
- 禁止选择器组件选择空值
|
||||||
|
- 修复proxy-provider
|
||||||
|
|
||||||
|
## 1.7.3
|
||||||
|
**注意:如安装后为英文,请在设置中反复选择几次不同语言以写入配置文件**
|
||||||
|
|
||||||
|
### 新功能 (Feat)
|
||||||
|
- Mihomo 内核升级 v1.19.5
|
||||||
|
- MacOS 下添加 Dock 图标动态展现方式 (#594)
|
||||||
|
- 更改默认 UA 并添加版本
|
||||||
|
- 添加固定间隔的配置文件更新按钮 (#670)
|
||||||
|
- 重构Linux上的手动授权内核方式
|
||||||
|
- 将sub-store迁移到工作目录下(#552)
|
||||||
|
- 重置软件增加警告提示
|
||||||
|
|
||||||
|
### 修复 (Fix)
|
||||||
|
- 修复代理节点页面因为重复刷新导致的溢出问题
|
||||||
|
- 修复由于 Mihomo 核心错误导致启动时窗口丢失 (#601)
|
||||||
|
- 修复macOS下的sub-store更新问题 (#552)
|
||||||
|
- 修复多语言翻译
|
||||||
|
- 修复 defaultBypass 几乎总是 Windows 默认绕过设置 (#602)
|
||||||
|
- 修复重置防火墙时发生的错误,因为没有指定防火墙规则 (#650)
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
appId: party.mihomo.app
|
appId: party.mihomo.app
|
||||||
productName: mihomo-party
|
productName: Mihomo Party
|
||||||
executableName: mihomo-party
|
|
||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
files:
|
files:
|
||||||
@ -31,7 +30,6 @@ win:
|
|||||||
artifactName: ${name}-windows-${version}-${arch}-portable.${ext}
|
artifactName: ${name}-windows-${version}-${arch}-portable.${ext}
|
||||||
nsis:
|
nsis:
|
||||||
artifactName: ${name}-windows-${version}-${arch}-setup.${ext}
|
artifactName: ${name}-windows-${version}-${arch}-setup.${ext}
|
||||||
shortcutName: Mihomo Party
|
|
||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
allowToChangeInstallationDirectory: true
|
allowToChangeInstallationDirectory: true
|
||||||
oneClick: false
|
oneClick: false
|
||||||
@ -39,25 +37,34 @@ nsis:
|
|||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
mac:
|
mac:
|
||||||
target:
|
target:
|
||||||
- dmg
|
- pkg
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
extendInfo:
|
extendInfo:
|
||||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
notarize: false
|
notarize: true
|
||||||
artifactName: ${name}-macos-${version}-${arch}.${ext}
|
artifactName: ${name}-macos-${version}-${arch}.${ext}
|
||||||
|
pkg:
|
||||||
|
allowAnywhere: false
|
||||||
|
allowCurrentUserHome: false
|
||||||
|
background:
|
||||||
|
alignment: bottomleft
|
||||||
|
file: build/background.png
|
||||||
linux:
|
linux:
|
||||||
desktop:
|
desktop:
|
||||||
|
Name: Mihomo Party
|
||||||
MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo'
|
MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo'
|
||||||
target:
|
target:
|
||||||
- deb
|
- deb
|
||||||
- rpm
|
- rpm
|
||||||
maintainer: mihomo-party
|
maintainer: mihomo-party-org
|
||||||
category: Utility
|
category: Utility
|
||||||
artifactName: ${name}-linux-${version}-${arch}.${ext}
|
artifactName: ${name}-linux-${version}-${arch}.${ext}
|
||||||
deb:
|
deb:
|
||||||
afterInstall: 'build/linux/deb/postinst'
|
afterInstall: 'build/linux/postinst'
|
||||||
|
rpm:
|
||||||
|
afterInstall: 'build/linux/postinst'
|
||||||
npmRebuild: true
|
npmRebuild: true
|
||||||
publish: []
|
publish: []
|
||||||
|
|||||||
@ -22,6 +22,14 @@ export default defineConfig({
|
|||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()]
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: resolve('src/renderer/index.html'),
|
||||||
|
floating: resolve('src/renderer/floating.html')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@renderer': resolve('src/renderer/src')
|
'@renderer': resolve('src/renderer/src')
|
||||||
|
|||||||
89
package.json
89
package.json
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "mihomo-party",
|
"name": "mihomo-party",
|
||||||
"version": "1.3.2",
|
"version": "1.7.7",
|
||||||
"description": "Mihomo Party",
|
"description": "Mihomo Party",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "mihomo-party",
|
"author": "mihomo-party-org",
|
||||||
"homepage": "https://mihomo.party",
|
"homepage": "https://mihomo.party",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
@ -15,6 +15,7 @@
|
|||||||
"updater": "node scripts/updater.mjs",
|
"updater": "node scripts/updater.mjs",
|
||||||
"checksum": "node scripts/checksum.mjs",
|
"checksum": "node scripts/checksum.mjs",
|
||||||
"telegram": "node scripts/telegram.mjs",
|
"telegram": "node scripts/telegram.mjs",
|
||||||
|
"artifact": "node scripts/artifact.mjs",
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"build:win": "electron-vite build && electron-builder --publish never --win",
|
"build:win": "electron-vite build && electron-builder --publish never --win",
|
||||||
@ -24,66 +25,74 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@mihomo-party/sysproxy": "^2.0.4",
|
"@heroui/react": "^2.6.14",
|
||||||
|
"@mihomo-party/sysproxy": "^2.0.7",
|
||||||
|
"@mihomo-party/sysproxy-darwin-arm64": "^2.0.7",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.1",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"express": "^4.21.0",
|
"express": "^5.0.1",
|
||||||
"lodash": "^4.17.21",
|
"i18next": "^24.2.2",
|
||||||
"recharts": "^2.12.7",
|
"iconv-lite": "^0.6.3",
|
||||||
|
"react-i18next": "^15.4.0",
|
||||||
"webdav": "^5.7.1",
|
"webdav": "^5.7.1",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"yaml": "^2.5.1"
|
"yaml": "^2.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
"@nextui-org/react": "^2.4.6",
|
"@types/adm-zip": "^0.5.6",
|
||||||
"@types/adm-zip": "^0.5.5",
|
"@types/express": "^5.0.0",
|
||||||
"@types/express": "^4.17.21",
|
"@types/node": "^22.13.1",
|
||||||
"@types/node": "^22.5.5",
|
|
||||||
"@types/pubsub-js": "^1.8.6",
|
"@types/pubsub-js": "^1.8.6",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^19.0.4",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@types/ws": "^8.5.12",
|
"@types/ws": "^8.5.13",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"cron-validator": "^1.3.1",
|
"cron-validator": "^1.3.1",
|
||||||
"driver.js": "^1.3.1",
|
"driver.js": "^1.3.5",
|
||||||
"electron": "^32.1.2",
|
"electron": "^34.0.2",
|
||||||
"electron-builder": "^25.0.5",
|
"electron-builder": "25.1.8",
|
||||||
"electron-vite": "^2.3.0",
|
"electron-vite": "^2.3.0",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "8.57.1",
|
||||||
"eslint-plugin-react": "^7.36.1",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"framer-motion": "^11.5.4",
|
"form-data": "^4.0.1",
|
||||||
"meta-json-schema": "^1.18.8",
|
"framer-motion": "12.0.11",
|
||||||
"monaco-yaml": "^5.2.2",
|
"lodash": "^4.17.21",
|
||||||
"nanoid": "^5.0.7",
|
"meta-json-schema": "^1.18.9",
|
||||||
"next-themes": "^0.3.0",
|
"monaco-yaml": "^5.2.3",
|
||||||
"postcss": "^8.4.45",
|
"nanoid": "^5.0.8",
|
||||||
|
"next-themes": "^0.4.3",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"pubsub-js": "^1.9.4",
|
"pubsub-js": "^1.9.5",
|
||||||
"react": "^18.3.1",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.0.0",
|
||||||
"react-error-boundary": "^4.0.13",
|
"react-error-boundary": "^5.0.0",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-monaco-editor": "^0.56.1",
|
"react-monaco-editor": "^0.58.0",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^7.1.5",
|
||||||
"react-virtuoso": "^4.10.4",
|
"react-virtuoso": "^4.12.0",
|
||||||
|
"recharts": "^2.13.3",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwindcss": "^3.4.11",
|
"tailwindcss": "^3.4.17",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"tsx": "^4.19.1",
|
"tsx": "^4.19.2",
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.4.5",
|
"vite": "^6.0.7",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0"
|
"vite-plugin-monaco-editor": "^1.1.0"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
|
||||||
}
|
}
|
||||||
|
|||||||
9635
pnpm-lock.yaml
generated
9635
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
27
scripts/cleanup-mac.sh
Executable file
27
scripts/cleanup-mac.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Mihomo Party Cleanup Tool ==="
|
||||||
|
echo "This script will remove all Mihomo Party related files and services."
|
||||||
|
read -p "Are you sure you want to continue? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Aborted."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop and unload services
|
||||||
|
echo "Stopping services..."
|
||||||
|
sudo launchctl unload /Library/LaunchDaemons/party.mihomo.helper.plist 2>/dev/null || true
|
||||||
|
|
||||||
|
# Remove files
|
||||||
|
echo "Removing files..."
|
||||||
|
sudo rm -f /Library/LaunchDaemons/party.mihomo.helper.plist
|
||||||
|
sudo rm -f /Library/PrivilegedHelperTools/party.mihomo.helper
|
||||||
|
sudo rm -rf "/Applications/Mihomo Party.app"
|
||||||
|
sudo rm -rf "/Applications/Mihomo\\ Party.app"
|
||||||
|
sudo rm -rf ~/Library/Application\ Support/mihomo-party
|
||||||
|
sudo rm -rf ~/Library/Caches/mihomo-party
|
||||||
|
sudo rm -f ~/Library/Preferences/party.mihomo.app.helper.plist
|
||||||
|
sudo rm -f ~/Library/Preferences/party.mihomo.app.plist
|
||||||
|
|
||||||
|
echo "Cleanup complete. Please restart your computer to complete the process."
|
||||||
@ -241,6 +241,11 @@ const resolveMmdb = () =>
|
|||||||
file: 'country.mmdb',
|
file: 'country.mmdb',
|
||||||
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb`
|
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb`
|
||||||
})
|
})
|
||||||
|
const resolveMetadb = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: 'geoip.metadb',
|
||||||
|
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb`
|
||||||
|
})
|
||||||
const resolveGeosite = () =>
|
const resolveGeosite = () =>
|
||||||
resolveResource({
|
resolveResource({
|
||||||
file: 'geosite.dat',
|
file: 'geosite.dat',
|
||||||
@ -271,6 +276,28 @@ const resolveRunner = () =>
|
|||||||
file: 'mihomo-party-run.exe',
|
file: 'mihomo-party-run.exe',
|
||||||
downloadURL: `https://github.com/mihomo-party-org/mihomo-party-run/releases/download/${arch}/mihomo-party-run.exe`
|
downloadURL: `https://github.com/mihomo-party-org/mihomo-party-run/releases/download/${arch}/mihomo-party-run.exe`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const resolveMonitor = async () => {
|
||||||
|
const tempDir = path.join(TEMP_DIR, 'TrafficMonitor')
|
||||||
|
const tempZip = path.join(tempDir, `${arch}.zip`)
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true })
|
||||||
|
}
|
||||||
|
await downloadFile(
|
||||||
|
`https://github.com/mihomo-party-org/mihomo-party-run/releases/download/monitor/${arch}.zip`,
|
||||||
|
tempZip
|
||||||
|
)
|
||||||
|
const zip = new AdmZip(tempZip)
|
||||||
|
const resDir = path.join(cwd, 'extra', 'files')
|
||||||
|
const targetPath = path.join(resDir, 'TrafficMonitor')
|
||||||
|
if (fs.existsSync(targetPath)) {
|
||||||
|
fs.rmSync(targetPath, { recursive: true })
|
||||||
|
}
|
||||||
|
zip.extractAllTo(targetPath, true)
|
||||||
|
|
||||||
|
console.log(`[INFO]: TrafficMonitor finished`)
|
||||||
|
}
|
||||||
|
|
||||||
const resolve7zip = () =>
|
const resolve7zip = () =>
|
||||||
resolveResource({
|
resolveResource({
|
||||||
file: '7za.exe',
|
file: '7za.exe',
|
||||||
@ -282,6 +309,11 @@ const resolveSubstore = () =>
|
|||||||
downloadURL:
|
downloadURL:
|
||||||
'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js'
|
'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js'
|
||||||
})
|
})
|
||||||
|
const resolveHelper = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: 'party.mihomo.helper',
|
||||||
|
downloadURL: `https://github.com/mihomo-party-org/mihomo-party-helper/releases/download/${arch}/party.mihomo.helper`
|
||||||
|
})
|
||||||
const resolveSubstoreFrontend = async () => {
|
const resolveSubstoreFrontend = async () => {
|
||||||
const tempDir = path.join(TEMP_DIR, 'substore-frontend')
|
const tempDir = path.join(TEMP_DIR, 'substore-frontend')
|
||||||
const tempZip = path.join(tempDir, 'dist.zip')
|
const tempZip = path.join(tempDir, 'dist.zip')
|
||||||
@ -329,6 +361,7 @@ const tasks = [
|
|||||||
retry: 5
|
retry: 5
|
||||||
},
|
},
|
||||||
{ name: 'mmdb', func: resolveMmdb, retry: 5 },
|
{ name: 'mmdb', func: resolveMmdb, retry: 5 },
|
||||||
|
{ name: 'metadb', func: resolveMetadb, retry: 5 },
|
||||||
{ name: 'geosite', func: resolveGeosite, retry: 5 },
|
{ name: 'geosite', func: resolveGeosite, retry: 5 },
|
||||||
{ name: 'geoip', func: resolveGeoIP, retry: 5 },
|
{ name: 'geoip', func: resolveGeoIP, retry: 5 },
|
||||||
{ name: 'asn', func: resolveASN, retry: 5 },
|
{ name: 'asn', func: resolveASN, retry: 5 },
|
||||||
@ -355,6 +388,12 @@ const tasks = [
|
|||||||
retry: 5,
|
retry: 5,
|
||||||
winOnly: true
|
winOnly: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'monitor',
|
||||||
|
func: resolveMonitor,
|
||||||
|
retry: 5,
|
||||||
|
winOnly: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'substore',
|
name: 'substore',
|
||||||
func: resolveSubstore,
|
func: resolveSubstore,
|
||||||
@ -370,6 +409,12 @@ const tasks = [
|
|||||||
func: resolve7zip,
|
func: resolve7zip,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
winOnly: true
|
winOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helper',
|
||||||
|
func: resolveHelper,
|
||||||
|
retry: 5,
|
||||||
|
darwinOnly: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -379,6 +424,7 @@ async function runTask() {
|
|||||||
if (task.winOnly && platform !== 'win32') return runTask()
|
if (task.winOnly && platform !== 'win32') return runTask()
|
||||||
if (task.linuxOnly && platform !== 'linux') return runTask()
|
if (task.linuxOnly && platform !== 'linux') return runTask()
|
||||||
if (task.unixOnly && platform === 'win32') return runTask()
|
if (task.unixOnly && platform === 'win32') return runTask()
|
||||||
|
if (task.darwinOnly && platform !== 'darwin') return runTask()
|
||||||
|
|
||||||
for (let i = 0; i < task.retry; i++) {
|
for (let i = 0; i < task.retry; i++) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
|
const chat_id = '@MihomoPartyChannel'
|
||||||
const pkg = readFileSync('package.json', 'utf-8')
|
const pkg = readFileSync('package.json', 'utf-8')
|
||||||
const changelog = readFileSync('changelog.md', 'utf-8')
|
const changelog = readFileSync('changelog.md', 'utf-8')
|
||||||
const { version } = JSON.parse(pkg)
|
const { version } = JSON.parse(pkg)
|
||||||
|
const downloadUrl = `https://github.com/mihomo-party-org/mihomo-party/releases/download/v${version}`
|
||||||
let content = `<b>🌟 <a href="https://github.com/mihomo-party-org/mihomo-party/releases/tag/v${version}">Mihomo Party v${version}</a> 正式发布</b>\n\n`
|
let content = `<b>🌟 <a href="https://github.com/mihomo-party-org/mihomo-party/releases/tag/v${version}">Mihomo Party v${version}</a> 正式发布</b>\n\n`
|
||||||
for (const line of changelog.split('\n')) {
|
for (const line of changelog.split('\n')) {
|
||||||
if (line.length === 0) {
|
if (line.length === 0) {
|
||||||
@ -14,8 +16,26 @@ for (const line of changelog.split('\n')) {
|
|||||||
content += `${line}\n`
|
content += `${line}\n`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
axios.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
|
|
||||||
chat_id: '@mihomo_party_channel',
|
content += '\n<b>下载地址:</b>\n<b>Windows10/11:</b>\n'
|
||||||
|
content += `安装版:<a href="${downloadUrl}/mihomo-party-windows-${version}-x64-setup.exe">64位</a> | <a href="${downloadUrl}/mihomo-party-windows-${version}-ia32-setup.exe">32位</a> | <a href="${downloadUrl}/mihomo-party-windows-${version}-arm64-setup.exe">ARM64</a>\n`
|
||||||
|
content += `便携版:<a href="${downloadUrl}/mihomo-party-windows-${version}-x64-portable.7z">64位</a> | <a href="${downloadUrl}/mihomo-party-windows-${version}-ia32-portable.7z">32位</a> | <a href="${downloadUrl}/mihomo-party-windows-${version}-arm64-portable.7z">ARM64</a>\n`
|
||||||
|
content += '\n<b>Windows7/8:</b>\n'
|
||||||
|
content += `安装版:<a href="${downloadUrl}/mihomo-party-win7-${version}-x64-setup.exe">64位</a> | <a href="${downloadUrl}/mihomo-party-win7-${version}-ia32-setup.exe">32位</a>\n`
|
||||||
|
content += `便携版:<a href="${downloadUrl}/mihomo-party-win7-${version}-x64-portable.7z">64位</a> | <a href="${downloadUrl}/mihomo-party-win7-${version}-ia32-portable.7z">32位</a>\n`
|
||||||
|
content += '\n<b>macOS 11+:</b>\n'
|
||||||
|
content += `PKG:<a href="${downloadUrl}/mihomo-party-macos-${version}-x64.pkg
|
||||||
|
">Intel</a> | <a href="${downloadUrl}/mihomo-party-macos-${version}-arm64.pkg">Apple Silicon</a>\n`
|
||||||
|
content += '\n<b>macOS 10.15+:</b>\n'
|
||||||
|
content += `PKG:<a href="${downloadUrl}/mihomo-party-catalina-${version}-x64.pkg
|
||||||
|
">Intel</a> | <a href="${downloadUrl}/mihomo-party-catalina-${version}-arm64.pkg">Apple Silicon</a>\n`
|
||||||
|
content += '\n<b>Linux:</b>\n'
|
||||||
|
content += `DEB:<a href="${downloadUrl}/mihomo-party-linux-${version}-amd64.deb
|
||||||
|
">64位</a> | <a href="${downloadUrl}/mihomo-party-linux-${version}-arm64.deb">ARM64</a>\n`
|
||||||
|
content += `RPM:<a href="${downloadUrl}/mihomo-party-linux-${version}-x86_64.rpm">64位</a> | <a href="${downloadUrl}/mihomo-party-linux-${version}-aarch64.rpm">ARM64</a>`
|
||||||
|
|
||||||
|
await axios.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
|
||||||
|
chat_id,
|
||||||
text: content,
|
text: content,
|
||||||
link_preview_options: {
|
link_preview_options: {
|
||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
|
|||||||
@ -2,11 +2,27 @@ import yaml from 'yaml'
|
|||||||
import { readFileSync, writeFileSync } from 'fs'
|
import { readFileSync, writeFileSync } from 'fs'
|
||||||
|
|
||||||
const pkg = readFileSync('package.json', 'utf-8')
|
const pkg = readFileSync('package.json', 'utf-8')
|
||||||
const changelog = readFileSync('changelog.md', 'utf-8')
|
let changelog = readFileSync('changelog.md', 'utf-8')
|
||||||
const { version } = JSON.parse(pkg)
|
const { version } = JSON.parse(pkg)
|
||||||
|
const downloadUrl = `https://github.com/mihomo-party-org/mihomo-party/releases/download/v${version}`
|
||||||
const latest = {
|
const latest = {
|
||||||
version,
|
version,
|
||||||
changelog
|
changelog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changelog += '\n### 下载地址:\n\n#### Windows10/11:\n\n'
|
||||||
|
changelog += `- 安装版:[64位](${downloadUrl}/mihomo-party-windows-${version}-x64-setup.exe) | [32位](${downloadUrl}/mihomo-party-windows-${version}-ia32-setup.exe) | [ARM64](${downloadUrl}/mihomo-party-windows-${version}-arm64-setup.exe)\n\n`
|
||||||
|
changelog += `- 便携版:[64位](${downloadUrl}/mihomo-party-windows-${version}-x64-portable.7z) | [32位](${downloadUrl}/mihomo-party-windows-${version}-ia32-portable.7z) | [ARM64](${downloadUrl}/mihomo-party-windows-${version}-arm64-portable.7z)\n\n`
|
||||||
|
changelog += '\n#### Windows7/8:\n\n'
|
||||||
|
changelog += `- 安装版:[64位](${downloadUrl}/mihomo-party-win7-${version}-x64-setup.exe) | [32位](${downloadUrl}/mihomo-party-win7-${version}-ia32-setup.exe)\n\n`
|
||||||
|
changelog += `- 便携版:[64位](${downloadUrl}/mihomo-party-win7-${version}-x64-portable.7z) | [32位](${downloadUrl}/mihomo-party-win7-${version}-ia32-portable.7z)\n\n`
|
||||||
|
changelog += '\n#### macOS 11+:\n\n'
|
||||||
|
changelog += `- PKG:[Intel](${downloadUrl}/mihomo-party-macos-${version}-x64.pkg) | [Apple Silicon](${downloadUrl}/mihomo-party-macos-${version}-arm64.pkg)\n\n`
|
||||||
|
changelog += '\n#### macOS 10.15+:\n\n'
|
||||||
|
changelog += `- PKG:[Intel](${downloadUrl}/mihomo-party-catalina-${version}-x64.pkg) | [Apple Silicon](${downloadUrl}/mihomo-party-catalina-${version}-arm64.pkg)\n\n`
|
||||||
|
changelog += '\n#### Linux:\n\n'
|
||||||
|
changelog += `- DEB:[64位](${downloadUrl}/mihomo-party-linux-${version}-amd64.deb) | [ARM64](${downloadUrl}/mihomo-party-linux-${version}-arm64.deb)\n\n`
|
||||||
|
changelog += `- RPM:[64位](${downloadUrl}/mihomo-party-linux-${version}-x86_64.rpm) | [ARM64](${downloadUrl}/mihomo-party-linux-${version}-aarch64.rpm)`
|
||||||
|
|
||||||
writeFileSync('latest.yml', yaml.stringify(latest))
|
writeFileSync('latest.yml', yaml.stringify(latest))
|
||||||
|
writeFileSync('changelog.md', changelog)
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { controledMihomoConfigPath } from '../utils/dirs'
|
import { controledMihomoConfigPath } from '../utils/dirs'
|
||||||
import { readFile, writeFile } from 'fs/promises'
|
import { readFile, writeFile } from 'fs/promises'
|
||||||
import yaml from 'yaml'
|
import yaml from 'yaml'
|
||||||
import { getAxios } from '../core/mihomoApi'
|
|
||||||
import { generateProfile } from '../core/factory'
|
import { generateProfile } from '../core/factory'
|
||||||
import { getAppConfig } from './app'
|
import { getAppConfig } from './app'
|
||||||
import { defaultControledMihomoConfig } from '../utils/template'
|
import { defaultControledMihomoConfig } from '../utils/template'
|
||||||
@ -52,9 +51,6 @@ export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>):
|
|||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
delete controledMihomoConfig?.tun?.device
|
delete controledMihomoConfig?.tun?.device
|
||||||
}
|
}
|
||||||
if (patch['external-controller'] || patch.secret) {
|
|
||||||
await getAxios(true)
|
|
||||||
}
|
|
||||||
await generateProfile()
|
await generateProfile()
|
||||||
await writeFile(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig), 'utf-8')
|
await writeFile(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig), 'utf-8')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ export {
|
|||||||
getCurrentProfileItem,
|
getCurrentProfileItem,
|
||||||
getProfileItem,
|
getProfileItem,
|
||||||
getProfileConfig,
|
getProfileConfig,
|
||||||
|
getFileStr,
|
||||||
|
setFileStr,
|
||||||
setProfileConfig,
|
setProfileConfig,
|
||||||
addProfileItem,
|
addProfileItem,
|
||||||
removeProfileItem,
|
removeProfileItem,
|
||||||
|
|||||||
@ -75,7 +75,8 @@ export async function createOverride(item: Partial<IOverrideItem>): Promise<IOve
|
|||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: mixedPort
|
port: mixedPort
|
||||||
}
|
},
|
||||||
|
responseType: 'text'
|
||||||
})
|
})
|
||||||
const data = res.data
|
const data = res.data
|
||||||
await setOverride(id, newItem.ext, data)
|
await setOverride(id, newItem.ext, data)
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
import { getControledMihomoConfig } from './controledMihomo'
|
import { getControledMihomoConfig } from './controledMihomo'
|
||||||
import { profileConfigPath, profilePath } from '../utils/dirs'
|
import { mihomoProfileWorkDir, mihomoWorkDir, profileConfigPath, profilePath } from '../utils/dirs'
|
||||||
import { addProfileUpdater } from '../core/profileUpdater'
|
import { addProfileUpdater } from '../core/profileUpdater'
|
||||||
import { readFile, rm, writeFile } from 'fs/promises'
|
import { readFile, rm, writeFile } from 'fs/promises'
|
||||||
import { restartCore } from '../core/manager'
|
import { restartCore } from '../core/manager'
|
||||||
import { getAppConfig } from './app'
|
import { getAppConfig } from './app'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import axios from 'axios'
|
import axios, { AxiosResponse } from 'axios'
|
||||||
import yaml from 'yaml'
|
import yaml from 'yaml'
|
||||||
import { defaultProfile } from '../utils/template'
|
import { defaultProfile } from '../utils/template'
|
||||||
|
import { subStorePort } from '../resolve/server'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { app } from 'electron'
|
||||||
|
|
||||||
let profileConfig: IProfileConfig // profile.yaml
|
let profileConfig: IProfileConfig // profile.yaml
|
||||||
|
|
||||||
@ -91,6 +94,9 @@ export async function removeProfileItem(id: string): Promise<void> {
|
|||||||
if (shouldRestart) {
|
if (shouldRestart) {
|
||||||
await restartCore()
|
await restartCore()
|
||||||
}
|
}
|
||||||
|
if (existsSync(mihomoProfileWorkDir(id))) {
|
||||||
|
await rm(mihomoProfileWorkDir(id), { recursive: true })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCurrentProfileItem(): Promise<IProfileItem> {
|
export async function getCurrentProfileItem(): Promise<IProfileItem> {
|
||||||
@ -105,9 +111,11 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
|
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
|
||||||
type: item.type,
|
type: item.type,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
|
substore: item.substore || false,
|
||||||
interval: item.interval || 0,
|
interval: item.interval || 0,
|
||||||
override: item.override || [],
|
override: item.override || [],
|
||||||
useProxy: item.useProxy || false,
|
useProxy: item.useProxy || false,
|
||||||
|
allowFixedInterval: item.allowFixedInterval || false,
|
||||||
updated: new Date().getTime()
|
updated: new Date().getTime()
|
||||||
} as IProfileItem
|
} as IProfileItem
|
||||||
switch (newItem.type) {
|
switch (newItem.type) {
|
||||||
@ -115,7 +123,24 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
const { userAgent } = await getAppConfig()
|
const { userAgent } = await getAppConfig()
|
||||||
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
|
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
|
||||||
if (!item.url) throw new Error('Empty URL')
|
if (!item.url) throw new Error('Empty URL')
|
||||||
const res = await axios.get(item.url, {
|
let res: AxiosResponse
|
||||||
|
if (newItem.substore) {
|
||||||
|
const urlObj = new URL(`http://127.0.0.1:${subStorePort}${item.url}`)
|
||||||
|
urlObj.searchParams.set('target', 'ClashMeta')
|
||||||
|
urlObj.searchParams.set('noCache', 'true')
|
||||||
|
if (newItem.useProxy) {
|
||||||
|
urlObj.searchParams.set('proxy', `http://127.0.0.1:${mixedPort}`)
|
||||||
|
} else {
|
||||||
|
urlObj.searchParams.delete('proxy')
|
||||||
|
}
|
||||||
|
res = await axios.get(urlObj.toString(), {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`
|
||||||
|
},
|
||||||
|
responseType: 'text'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res = await axios.get(item.url, {
|
||||||
proxy: newItem.useProxy
|
proxy: newItem.useProxy
|
||||||
? {
|
? {
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
@ -124,9 +149,12 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
}
|
}
|
||||||
: false,
|
: false,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': userAgent || 'clash.meta'
|
'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`
|
||||||
}
|
},
|
||||||
|
responseType: 'text'
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const data = res.data
|
const data = res.data
|
||||||
const headers = res.headers
|
const headers = res.headers
|
||||||
if (headers['content-disposition'] && newItem.name === 'Remote File') {
|
if (headers['content-disposition'] && newItem.name === 'Remote File') {
|
||||||
@ -136,8 +164,10 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
newItem.home = headers['profile-web-page-url']
|
newItem.home = headers['profile-web-page-url']
|
||||||
}
|
}
|
||||||
if (headers['profile-update-interval']) {
|
if (headers['profile-update-interval']) {
|
||||||
|
if (!item.allowFixedInterval) {
|
||||||
newItem.interval = parseInt(headers['profile-update-interval']) * 60
|
newItem.interval = parseInt(headers['profile-update-interval']) * 60
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (headers['subscription-userinfo']) {
|
if (headers['subscription-userinfo']) {
|
||||||
newItem.extra = parseSubinfo(headers['subscription-userinfo'])
|
newItem.extra = parseSubinfo(headers['subscription-userinfo'])
|
||||||
}
|
}
|
||||||
@ -195,3 +225,34 @@ function parseSubinfo(str: string): ISubscriptionUserInfo {
|
|||||||
})
|
})
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAbsolutePath(path: string): boolean {
|
||||||
|
return path.startsWith('/') || /^[a-zA-Z]:\\/.test(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFileStr(path: string): Promise<string> {
|
||||||
|
const { diffWorkDir = false } = await getAppConfig()
|
||||||
|
const { current } = await getProfileConfig()
|
||||||
|
if (isAbsolutePath(path)) {
|
||||||
|
return await readFile(path, 'utf-8')
|
||||||
|
} else {
|
||||||
|
return await readFile(
|
||||||
|
join(diffWorkDir ? mihomoProfileWorkDir(current) : mihomoWorkDir(), path),
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setFileStr(path: string, content: string): Promise<void> {
|
||||||
|
const { diffWorkDir = false } = await getAppConfig()
|
||||||
|
const { current } = await getProfileConfig()
|
||||||
|
if (isAbsolutePath(path)) {
|
||||||
|
await writeFile(path, content, 'utf-8')
|
||||||
|
} else {
|
||||||
|
await writeFile(
|
||||||
|
join(diffWorkDir ? mihomoProfileWorkDir(current) : mihomoWorkDir(), path),
|
||||||
|
content,
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -5,28 +5,65 @@ import {
|
|||||||
getProfileItem,
|
getProfileItem,
|
||||||
getOverride,
|
getOverride,
|
||||||
getOverrideItem,
|
getOverrideItem,
|
||||||
getOverrideConfig
|
getOverrideConfig,
|
||||||
|
getAppConfig
|
||||||
} from '../config'
|
} from '../config'
|
||||||
import { mihomoWorkConfigPath, overridePath } from '../utils/dirs'
|
import {
|
||||||
|
mihomoProfileWorkDir,
|
||||||
|
mihomoWorkConfigPath,
|
||||||
|
mihomoWorkDir,
|
||||||
|
overridePath
|
||||||
|
} from '../utils/dirs'
|
||||||
import yaml from 'yaml'
|
import yaml from 'yaml'
|
||||||
import { writeFile } from 'fs/promises'
|
import { copyFile, mkdir, writeFile } from 'fs/promises'
|
||||||
import { deepMerge } from '../utils/merge'
|
import { deepMerge } from '../utils/merge'
|
||||||
import vm from 'vm'
|
import vm from 'vm'
|
||||||
import { writeFileSync } from 'fs'
|
import { existsSync, writeFileSync } from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
let runtimeConfigStr: string
|
let runtimeConfigStr: string
|
||||||
let runtimeConfig: IMihomoConfig
|
let runtimeConfig: IMihomoConfig
|
||||||
|
|
||||||
export async function generateProfile(): Promise<void> {
|
export async function generateProfile(): Promise<void> {
|
||||||
const { current } = await getProfileConfig()
|
const { current } = await getProfileConfig()
|
||||||
|
const { diffWorkDir = false } = await getAppConfig()
|
||||||
const currentProfile = await overrideProfile(current, await getProfile(current))
|
const currentProfile = await overrideProfile(current, await getProfile(current))
|
||||||
const controledMihomoConfig = await getControledMihomoConfig()
|
const controledMihomoConfig = await getControledMihomoConfig()
|
||||||
const profile = deepMerge(currentProfile, controledMihomoConfig)
|
const profile = deepMerge(currentProfile, controledMihomoConfig)
|
||||||
// 确保可以拿到基础日志信息
|
// 确保可以拿到基础日志信息
|
||||||
|
// 使用 debug 可以调试内核相关问题 `debug/pprof`
|
||||||
|
if (['info', 'debug'].includes(profile['log-level']) === false) {
|
||||||
profile['log-level'] = 'info'
|
profile['log-level'] = 'info'
|
||||||
|
}
|
||||||
runtimeConfig = profile
|
runtimeConfig = profile
|
||||||
runtimeConfigStr = yaml.stringify(profile)
|
runtimeConfigStr = yaml.stringify(profile)
|
||||||
await writeFile(mihomoWorkConfigPath(), runtimeConfigStr)
|
if (diffWorkDir) {
|
||||||
|
await prepareProfileWorkDir(current)
|
||||||
|
}
|
||||||
|
await writeFile(
|
||||||
|
diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'),
|
||||||
|
runtimeConfigStr
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareProfileWorkDir(current: string | undefined): Promise<void> {
|
||||||
|
if (!existsSync(mihomoProfileWorkDir(current))) {
|
||||||
|
await mkdir(mihomoProfileWorkDir(current), { recursive: true })
|
||||||
|
}
|
||||||
|
const copy = async (file: string): Promise<void> => {
|
||||||
|
const targetPath = path.join(mihomoProfileWorkDir(current), file)
|
||||||
|
const sourcePath = path.join(mihomoWorkDir(), file)
|
||||||
|
if (!existsSync(targetPath) && existsSync(sourcePath)) {
|
||||||
|
await copyFile(sourcePath, targetPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all([
|
||||||
|
copy('country.mmdb'),
|
||||||
|
copy('geoip.metadb'),
|
||||||
|
copy('geoip.dat'),
|
||||||
|
copy('geosite.dat'),
|
||||||
|
copy('ASN.mmdb')
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
async function overrideProfile(
|
async function overrideProfile(
|
||||||
@ -92,7 +129,7 @@ function runOverrideScript(
|
|||||||
log('info', '脚本执行成功')
|
log('info', '脚本执行成功')
|
||||||
return newProfile
|
return newProfile
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('exception', `脚本执行失败: ${e}`)
|
log('exception', `脚本执行失败:${e}`)
|
||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
logPath,
|
logPath,
|
||||||
mihomoCoreDir,
|
mihomoCoreDir,
|
||||||
mihomoCorePath,
|
mihomoCorePath,
|
||||||
|
mihomoProfileWorkDir,
|
||||||
mihomoTestDir,
|
mihomoTestDir,
|
||||||
mihomoWorkConfigPath,
|
mihomoWorkConfigPath,
|
||||||
mihomoWorkDir
|
mihomoWorkDir
|
||||||
@ -12,10 +13,11 @@ import { generateProfile } from './factory'
|
|||||||
import {
|
import {
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
getControledMihomoConfig,
|
getControledMihomoConfig,
|
||||||
|
getProfileConfig,
|
||||||
patchAppConfig,
|
patchAppConfig,
|
||||||
patchControledMihomoConfig
|
patchControledMihomoConfig
|
||||||
} from '../config'
|
} from '../config'
|
||||||
import { app, dialog, ipcMain, net, safeStorage } from 'electron'
|
import { app, dialog, ipcMain, net } from 'electron'
|
||||||
import {
|
import {
|
||||||
startMihomoTraffic,
|
startMihomoTraffic,
|
||||||
startMihomoConnections,
|
startMihomoConnections,
|
||||||
@ -32,48 +34,55 @@ import { readFile, rm, writeFile } from 'fs/promises'
|
|||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { mainWindow } from '..'
|
import { mainWindow } from '..'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { existsSync } from 'fs'
|
import os from 'os'
|
||||||
|
import { createWriteStream, existsSync } from 'fs'
|
||||||
import { uploadRuntimeConfig } from '../resolve/gistApi'
|
import { uploadRuntimeConfig } from '../resolve/gistApi'
|
||||||
|
import { startMonitor } from '../resolve/trafficMonitor'
|
||||||
|
import i18next from '../../shared/i18n'
|
||||||
|
|
||||||
chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => {
|
chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => {
|
||||||
try {
|
try {
|
||||||
await stopCore(true)
|
await stopCore(true)
|
||||||
await startCore()
|
await startCore()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dialog.showErrorBox('内核启动出错', `${e}`)
|
dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const mihomoIpcPath =
|
||||||
|
process.platform === 'win32' ? '\\\\.\\pipe\\MihomoParty\\mihomo' : '/tmp/mihomo-party.sock'
|
||||||
|
const ctlParam = process.platform === 'win32' ? '-ext-ctl-pipe' : '-ext-ctl-unix'
|
||||||
|
|
||||||
let setPublicDNSTimer: NodeJS.Timeout | null = null
|
let setPublicDNSTimer: NodeJS.Timeout | null = null
|
||||||
let recoverDNSTimer: NodeJS.Timeout | null = null
|
let recoverDNSTimer: NodeJS.Timeout | null = null
|
||||||
let child: ChildProcess
|
let child: ChildProcess
|
||||||
let retry = 10
|
let retry = 10
|
||||||
|
|
||||||
export async function startCore(detached = false): Promise<Promise<void>[]> {
|
export async function startCore(detached = false): Promise<Promise<void>[]> {
|
||||||
const { core = 'mihomo', autoSetDNS = true, encryptedPassword } = await getAppConfig()
|
const {
|
||||||
|
core = 'mihomo',
|
||||||
|
autoSetDNS = true,
|
||||||
|
diffWorkDir = false,
|
||||||
|
mihomoCpuPriority = 'PRIORITY_NORMAL',
|
||||||
|
disableLoopbackDetector = false,
|
||||||
|
disableEmbedCA = false,
|
||||||
|
disableSystemCA = false,
|
||||||
|
skipSafePathCheck = false
|
||||||
|
} = await getAppConfig()
|
||||||
const { 'log-level': logLevel } = await getControledMihomoConfig()
|
const { 'log-level': logLevel } = await getControledMihomoConfig()
|
||||||
if (existsSync(path.join(dataDir(), 'core.pid'))) {
|
if (existsSync(path.join(dataDir(), 'core.pid'))) {
|
||||||
const pid = parseInt(await readFile(path.join(dataDir(), 'core.pid'), 'utf-8'))
|
const pid = parseInt(await readFile(path.join(dataDir(), 'core.pid'), 'utf-8'))
|
||||||
try {
|
try {
|
||||||
process.kill(pid, 'SIGINT')
|
process.kill(pid, 'SIGINT')
|
||||||
} catch {
|
|
||||||
if (process.platform !== 'win32' && encryptedPassword && isEncryptionAvailable()) {
|
|
||||||
const execPromise = promisify(exec)
|
|
||||||
const password = safeStorage.decryptString(Buffer.from(encryptedPassword))
|
|
||||||
try {
|
|
||||||
await execPromise(`echo "${password}" | sudo -S kill ${pid}`)
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
await rm(path.join(dataDir(), 'core.pid'))
|
await rm(path.join(dataDir(), 'core.pid'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const { current } = await getProfileConfig()
|
||||||
const { tun } = await getControledMihomoConfig()
|
const { tun } = await getControledMihomoConfig()
|
||||||
const corePath = mihomoCorePath(core)
|
const corePath = mihomoCorePath(core)
|
||||||
await autoGrantCorePermition(corePath)
|
|
||||||
await generateProfile()
|
await generateProfile()
|
||||||
await checkProfile()
|
await checkProfile()
|
||||||
await stopCore()
|
await stopCore()
|
||||||
@ -86,10 +95,26 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
child = spawn(corePath, ['-d', mihomoWorkDir()], {
|
const stdout = createWriteStream(logPath(), { flags: 'a' })
|
||||||
|
const stderr = createWriteStream(logPath(), { flags: 'a' })
|
||||||
|
const env = {
|
||||||
|
DISABLE_LOOPBACK_DETECTOR: String(disableLoopbackDetector),
|
||||||
|
DISABLE_EMBED_CA: String(disableEmbedCA),
|
||||||
|
DISABLE_SYSTEM_CA: String(disableSystemCA),
|
||||||
|
SKIP_SAFE_PATH_CHECK: String(skipSafePathCheck)
|
||||||
|
}
|
||||||
|
child = spawn(
|
||||||
|
corePath,
|
||||||
|
['-d', diffWorkDir ? mihomoProfileWorkDir(current) : mihomoWorkDir(), ctlParam, mihomoIpcPath],
|
||||||
|
{
|
||||||
detached: detached,
|
detached: detached,
|
||||||
stdio: detached ? 'ignore' : undefined
|
stdio: detached ? 'ignore' : undefined,
|
||||||
})
|
env: env
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (process.platform === 'win32' && child.pid) {
|
||||||
|
os.setPriority(child.pid, os.constants.priority[mihomoCpuPriority])
|
||||||
|
}
|
||||||
if (detached) {
|
if (detached) {
|
||||||
child.unref()
|
child.unref()
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@ -108,36 +133,35 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
await stopCore()
|
await stopCore()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
child.stdout?.on('data', async (data) => {
|
child.stdout?.pipe(stdout)
|
||||||
await writeFile(logPath(), data, { flag: 'a' })
|
child.stderr?.pipe(stderr)
|
||||||
})
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
child.stdout?.on('data', async (data) => {
|
child.stdout?.on('data', async (data) => {
|
||||||
if (data.toString().includes('configure tun interface: operation not permitted')) {
|
const str = data.toString()
|
||||||
|
if (str.includes('configure tun interface: operation not permitted')) {
|
||||||
patchControledMihomoConfig({ tun: { enable: false } })
|
patchControledMihomoConfig({ tun: { enable: false } })
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
reject('虚拟网卡启动失败, 请尝试手动授予内核权限')
|
reject(i18next.t('tun.error.tunPermissionDenied'))
|
||||||
}
|
}
|
||||||
if (data.toString().includes('External controller listen error')) {
|
|
||||||
if (retry) {
|
if ((process.platform !== 'win32' && str.includes('External controller unix listen error')) ||
|
||||||
retry--
|
(process.platform === 'win32' && str.includes('External controller pipe listen error'))
|
||||||
try {
|
) {
|
||||||
resolve(await startCore())
|
reject(i18next.t('mihomo.error.externalControllerListenError'))
|
||||||
} catch (e) {
|
|
||||||
reject(e)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
reject('内核连接失败, 请尝试修改外部控制端口或重启电脑')
|
if (
|
||||||
}
|
(process.platform !== 'win32' && str.includes('RESTful API unix listening at')) ||
|
||||||
}
|
(process.platform === 'win32' && str.includes('RESTful API pipe listening at'))
|
||||||
if (data.toString().includes('RESTful API listening at')) {
|
) {
|
||||||
resolve([
|
resolve([
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
child.stdout?.on('data', async (data) => {
|
child.stdout?.on('data', async (data) => {
|
||||||
if (data.toString().includes('Start initial Compatible provider default')) {
|
if (data.toString().toLowerCase().includes('start initial compatible provider default')) {
|
||||||
try {
|
try {
|
||||||
mainWindow?.webContents.send('coreRestart')
|
mainWindow?.webContents.send('groupsUpdated')
|
||||||
|
mainWindow?.webContents.send('rulesUpdated')
|
||||||
await uploadRuntimeConfig()
|
await uploadRuntimeConfig()
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
@ -183,7 +207,7 @@ export async function restartCore(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
await startCore()
|
await startCore()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dialog.showErrorBox('内核启动出错', `${e}`)
|
dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,21 +218,36 @@ export async function keepCoreAlive(): Promise<void> {
|
|||||||
await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString())
|
await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString())
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dialog.showErrorBox('内核启动出错', `${e}`)
|
dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function quitWithoutCore(): Promise<void> {
|
export async function quitWithoutCore(): Promise<void> {
|
||||||
await keepCoreAlive()
|
await keepCoreAlive()
|
||||||
|
await startMonitor(true)
|
||||||
app.exit()
|
app.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkProfile(): Promise<void> {
|
async function checkProfile(): Promise<void> {
|
||||||
const { core = 'mihomo' } = await getAppConfig()
|
const {
|
||||||
|
core = 'mihomo',
|
||||||
|
diffWorkDir = false,
|
||||||
|
skipSafePathCheck = false
|
||||||
|
} = await getAppConfig()
|
||||||
|
const { current } = await getProfileConfig()
|
||||||
const corePath = mihomoCorePath(core)
|
const corePath = mihomoCorePath(core)
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
|
const env = {
|
||||||
|
SKIP_SAFE_PATH_CHECK: String(skipSafePathCheck)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await execFilePromise(corePath, ['-t', '-f', mihomoWorkConfigPath(), '-d', mihomoTestDir()])
|
await execFilePromise(corePath, [
|
||||||
|
'-t',
|
||||||
|
'-f',
|
||||||
|
diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'),
|
||||||
|
'-d',
|
||||||
|
mihomoTestDir()
|
||||||
|
], { env })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && 'stdout' in error) {
|
if (error instanceof Error && 'stdout' in error) {
|
||||||
const { stdout } = error as { stdout: string }
|
const { stdout } = error as { stdout: string }
|
||||||
@ -216,71 +255,45 @@ async function checkProfile(): Promise<void> {
|
|||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((line) => line.includes('level=error'))
|
.filter((line) => line.includes('level=error'))
|
||||||
.map((line) => line.split('level=error')[1])
|
.map((line) => line.split('level=error')[1])
|
||||||
throw new Error(`Profile Check Failed:\n${errorLines.join('\n')}`)
|
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${errorLines.join('\n')}`)
|
||||||
} else {
|
} else {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function autoGrantCorePermition(corePath: string): Promise<void> {
|
export async function manualGrantCorePermition(): Promise<void> {
|
||||||
if (process.platform === 'win32') return
|
|
||||||
const { encryptedPassword } = await getAppConfig()
|
|
||||||
const execPromise = promisify(exec)
|
|
||||||
if (encryptedPassword && isEncryptionAvailable()) {
|
|
||||||
try {
|
|
||||||
const password = safeStorage.decryptString(Buffer.from(encryptedPassword))
|
|
||||||
if (process.platform === 'linux') {
|
|
||||||
await execPromise(`echo "${password}" | sudo -S chown root:root "${corePath}"`)
|
|
||||||
await execPromise(`echo "${password}" | sudo -S chmod +sx "${corePath}"`)
|
|
||||||
}
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
await execPromise(`echo "${password}" | sudo -S chown root:admin "${corePath}"`)
|
|
||||||
await execPromise(`echo "${password}" | sudo -S chmod +sx "${corePath}"`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
patchAppConfig({ encryptedPassword: undefined })
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function manualGrantCorePermition(password?: string): Promise<void> {
|
|
||||||
const { core = 'mihomo' } = await getAppConfig()
|
const { core = 'mihomo' } = await getAppConfig()
|
||||||
const corePath = mihomoCorePath(core)
|
const corePath = mihomoCorePath(core)
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
|
const execFilePromise = promisify(execFile)
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}`
|
const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}`
|
||||||
const command = `do shell script "${shell}" with administrator privileges`
|
const command = `do shell script "${shell}" with administrator privileges`
|
||||||
await execPromise(`osascript -e '${command}'`)
|
await execPromise(`osascript -e '${command}'`)
|
||||||
}
|
}
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
await execPromise(`echo "${password}" | sudo -S chown root:root "${corePath}"`)
|
await execFilePromise('pkexec', [
|
||||||
await execPromise(`echo "${password}" | sudo -S chmod +sx "${corePath}"`)
|
'bash',
|
||||||
|
'-c',
|
||||||
|
`chown root:root "${corePath}" && chmod +sx "${corePath}"`
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isEncryptionAvailable(): boolean {
|
export async function getDefaultDevice(): Promise<string> {
|
||||||
return safeStorage.isEncryptionAvailable()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDefaultDevice(password?: string): Promise<string> {
|
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
let sudo = ''
|
const { stdout: deviceOut } = await execPromise(`route -n get default`)
|
||||||
if (password) sudo = `echo "${password}" | sudo -S `
|
|
||||||
const { stdout: deviceOut } = await execPromise(`${sudo}route -n get default`)
|
|
||||||
let device = deviceOut.split('\n').find((s) => s.includes('interface:'))
|
let device = deviceOut.split('\n').find((s) => s.includes('interface:'))
|
||||||
device = device?.trim().split(' ').slice(1).join(' ')
|
device = device?.trim().split(' ').slice(1).join(' ')
|
||||||
if (!device) throw new Error('Get device failed')
|
if (!device) throw new Error('Get device failed')
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDefaultService(password?: string): Promise<string> {
|
async function getDefaultService(): Promise<string> {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
let sudo = ''
|
const device = await getDefaultDevice()
|
||||||
if (password) sudo = `echo "${password}" | sudo -S `
|
const { stdout: order } = await execPromise(`networksetup -listnetworkserviceorder`)
|
||||||
const device = await getDefaultDevice(password)
|
|
||||||
const { stdout: order } = await execPromise(`${sudo}networksetup -listnetworkserviceorder`)
|
|
||||||
const block = order.split('\n\n').find((s) => s.includes(`Device: ${device}`))
|
const block = order.split('\n\n').find((s) => s.includes(`Device: ${device}`))
|
||||||
if (!block) throw new Error('Get networkservice failed')
|
if (!block) throw new Error('Get networkservice failed')
|
||||||
for (const line of block.split('\n')) {
|
for (const line of block.split('\n')) {
|
||||||
@ -291,12 +304,10 @@ async function getDefaultService(password?: string): Promise<string> {
|
|||||||
throw new Error('Get service failed')
|
throw new Error('Get service failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOriginDNS(password?: string): Promise<void> {
|
async function getOriginDNS(): Promise<void> {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
let sudo = ''
|
const service = await getDefaultService()
|
||||||
if (password) sudo = `echo "${password}" | sudo -S `
|
const { stdout: dns } = await execPromise(`networksetup -getdnsservers "${service}"`)
|
||||||
const service = await getDefaultService(password)
|
|
||||||
const { stdout: dns } = await execPromise(`${sudo}networksetup -getdnsservers "${service}"`)
|
|
||||||
if (dns.startsWith("There aren't any DNS Servers set on")) {
|
if (dns.startsWith("There aren't any DNS Servers set on")) {
|
||||||
await patchAppConfig({ originDNS: 'Empty' })
|
await patchAppConfig({ originDNS: 'Empty' })
|
||||||
} else {
|
} else {
|
||||||
@ -304,25 +315,19 @@ async function getOriginDNS(password?: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setDNS(dns: string, password?: string): Promise<void> {
|
async function setDNS(dns: string): Promise<void> {
|
||||||
const service = await getDefaultService(password)
|
const service = await getDefaultService()
|
||||||
let sudo = ''
|
|
||||||
if (password) sudo = `echo "${password}" | sudo -S `
|
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
await execPromise(`${sudo}networksetup -setdnsservers "${service}" ${dns}`)
|
await execPromise(`networksetup -setdnsservers "${service}" ${dns}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setPublicDNS(): Promise<void> {
|
async function setPublicDNS(): Promise<void> {
|
||||||
if (process.platform !== 'darwin') return
|
if (process.platform !== 'darwin') return
|
||||||
if (net.isOnline()) {
|
if (net.isOnline()) {
|
||||||
const { originDNS, encryptedPassword } = await getAppConfig()
|
const { originDNS } = await getAppConfig()
|
||||||
if (!originDNS) {
|
if (!originDNS) {
|
||||||
let password: string | undefined
|
await getOriginDNS()
|
||||||
if (encryptedPassword && isEncryptionAvailable()) {
|
await setDNS('223.5.5.5')
|
||||||
password = safeStorage.decryptString(Buffer.from(encryptedPassword))
|
|
||||||
}
|
|
||||||
await getOriginDNS(password)
|
|
||||||
await setDNS('223.5.5.5', password)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (setPublicDNSTimer) clearTimeout(setPublicDNSTimer)
|
if (setPublicDNSTimer) clearTimeout(setPublicDNSTimer)
|
||||||
@ -333,13 +338,9 @@ async function setPublicDNS(): Promise<void> {
|
|||||||
async function recoverDNS(): Promise<void> {
|
async function recoverDNS(): Promise<void> {
|
||||||
if (process.platform !== 'darwin') return
|
if (process.platform !== 'darwin') return
|
||||||
if (net.isOnline()) {
|
if (net.isOnline()) {
|
||||||
const { originDNS, encryptedPassword } = await getAppConfig()
|
const { originDNS } = await getAppConfig()
|
||||||
if (originDNS) {
|
if (originDNS) {
|
||||||
let password: string | undefined
|
await setDNS(originDNS)
|
||||||
if (encryptedPassword && isEncryptionAvailable()) {
|
|
||||||
password = safeStorage.decryptString(Buffer.from(encryptedPassword))
|
|
||||||
}
|
|
||||||
await setDNS(originDNS, password)
|
|
||||||
await patchAppConfig({ originDNS: undefined })
|
await patchAppConfig({ originDNS: undefined })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import WebSocket from 'ws'
|
|||||||
import { tray } from '../resolve/tray'
|
import { tray } from '../resolve/tray'
|
||||||
import { calcTraffic } from '../utils/calc'
|
import { calcTraffic } from '../utils/calc'
|
||||||
import { getRuntimeConfig } from './factory'
|
import { getRuntimeConfig } from './factory'
|
||||||
|
import { floatingWindow } from '../resolve/floatingWindow'
|
||||||
|
import { mihomoIpcPath } from './manager'
|
||||||
|
|
||||||
let axiosIns: AxiosInstance = null!
|
let axiosIns: AxiosInstance = null!
|
||||||
let mihomoTrafficWs: WebSocket | null = null
|
let mihomoTrafficWs: WebSocket | null = null
|
||||||
@ -18,15 +20,10 @@ let connectionsRetry = 10
|
|||||||
|
|
||||||
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
|
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
|
||||||
if (axiosIns && !force) return axiosIns
|
if (axiosIns && !force) return axiosIns
|
||||||
const controledMihomoConfig = await getControledMihomoConfig()
|
|
||||||
let server = controledMihomoConfig['external-controller']
|
|
||||||
const secret = controledMihomoConfig.secret ?? ''
|
|
||||||
if (server?.startsWith(':')) server = `127.0.0.1${server}`
|
|
||||||
|
|
||||||
axiosIns = axios.create({
|
axiosIns = axios.create({
|
||||||
baseURL: `http://${server}`,
|
baseURL: `http://localhost`,
|
||||||
proxy: false,
|
socketPath: mihomoIpcPath,
|
||||||
headers: secret ? { Authorization: `Bearer ${secret}` } : {},
|
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -79,6 +76,8 @@ export const mihomoProxies = async (): Promise<IMihomoProxies> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const mihomoGroups = async (): Promise<IMihomoMixedGroup[]> => {
|
export const mihomoGroups = async (): Promise<IMihomoMixedGroup[]> => {
|
||||||
|
const { mode = 'rule' } = await getControledMihomoConfig()
|
||||||
|
if (mode === 'direct') return []
|
||||||
const proxies = await mihomoProxies()
|
const proxies = await mihomoProxies()
|
||||||
const runtime = await getRuntimeConfig()
|
const runtime = await getRuntimeConfig()
|
||||||
const groups: IMihomoMixedGroup[] = []
|
const groups: IMihomoMixedGroup[] = []
|
||||||
@ -98,6 +97,10 @@ export const mihomoGroups = async (): Promise<IMihomoMixedGroup[]> => {
|
|||||||
groups.push({ ...newGlobal, all: newAll })
|
groups.push({ ...newGlobal, all: newAll })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (mode === 'global') {
|
||||||
|
const global = groups.findIndex((group) => group.name === 'GLOBAL')
|
||||||
|
groups.unshift(groups.splice(global, 1)[0])
|
||||||
|
}
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,6 +129,11 @@ export const mihomoChangeProxy = async (group: string, proxy: string): Promise<I
|
|||||||
return await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy })
|
return await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mihomoUnfixedProxy = async (group: string): Promise<IMihomoProxy> => {
|
||||||
|
const instance = await getAxios()
|
||||||
|
return await instance.delete(`/proxies/${encodeURIComponent(group)}`)
|
||||||
|
}
|
||||||
|
|
||||||
export const mihomoUpgradeGeo = async (): Promise<void> => {
|
export const mihomoUpgradeGeo = async (): Promise<void> => {
|
||||||
const instance = await getAxios()
|
const instance = await getAxios()
|
||||||
return await instance.post('/configs/geo')
|
return await instance.post('/configs/geo')
|
||||||
@ -137,7 +145,7 @@ export const mihomoProxyDelay = async (proxy: string, url?: string): Promise<IMi
|
|||||||
const instance = await getAxios()
|
const instance = await getAxios()
|
||||||
return await instance.get(`/proxies/${encodeURIComponent(proxy)}/delay`, {
|
return await instance.get(`/proxies/${encodeURIComponent(proxy)}/delay`, {
|
||||||
params: {
|
params: {
|
||||||
url: url || delayTestUrl || 'https://www.gstatic.com/generate_204',
|
url: url || delayTestUrl || 'http://www.gstatic.com/generate_204',
|
||||||
timeout: delayTestTimeout || 5000
|
timeout: delayTestTimeout || 5000
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -149,7 +157,7 @@ export const mihomoGroupDelay = async (group: string, url?: string): Promise<IMi
|
|||||||
const instance = await getAxios()
|
const instance = await getAxios()
|
||||||
return await instance.get(`/group/${encodeURIComponent(group)}/delay`, {
|
return await instance.get(`/group/${encodeURIComponent(group)}/delay`, {
|
||||||
params: {
|
params: {
|
||||||
url: url || delayTestUrl || 'https://www.gstatic.com/generate_204',
|
url: url || delayTestUrl || 'http://www.gstatic.com/generate_204',
|
||||||
timeout: delayTestTimeout || 5000
|
timeout: delayTestTimeout || 5000
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -175,13 +183,7 @@ export const stopMihomoTraffic = (): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mihomoTraffic = async (): Promise<void> => {
|
const mihomoTraffic = async (): Promise<void> => {
|
||||||
const controledMihomoConfig = await getControledMihomoConfig()
|
mihomoTrafficWs = new WebSocket(`ws+unix:${mihomoIpcPath}:/traffic`)
|
||||||
let server = controledMihomoConfig['external-controller']
|
|
||||||
const secret = controledMihomoConfig.secret ?? ''
|
|
||||||
if (server?.startsWith(':')) server = `127.0.0.1${server}`
|
|
||||||
stopMihomoTraffic()
|
|
||||||
|
|
||||||
mihomoTrafficWs = new WebSocket(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`)
|
|
||||||
|
|
||||||
mihomoTrafficWs.onmessage = async (e): Promise<void> => {
|
mihomoTrafficWs.onmessage = async (e): Promise<void> => {
|
||||||
const data = e.data as string
|
const data = e.data as string
|
||||||
@ -197,6 +199,7 @@ const mihomoTraffic = async (): Promise<void> => {
|
|||||||
`${calcTraffic(json.down)}/s`.padStart(9)
|
`${calcTraffic(json.down)}/s`.padStart(9)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
floatingWindow?.webContents.send('mihomoTraffic', json)
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@ -232,13 +235,7 @@ export const stopMihomoMemory = (): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mihomoMemory = async (): Promise<void> => {
|
const mihomoMemory = async (): Promise<void> => {
|
||||||
const controledMihomoConfig = await getControledMihomoConfig()
|
mihomoMemoryWs = new WebSocket(`ws+unix:${mihomoIpcPath}:/memory`)
|
||||||
let server = controledMihomoConfig['external-controller']
|
|
||||||
const secret = controledMihomoConfig.secret ?? ''
|
|
||||||
if (server?.startsWith(':')) server = `127.0.0.1${server}`
|
|
||||||
stopMihomoMemory()
|
|
||||||
|
|
||||||
mihomoMemoryWs = new WebSocket(`ws://${server}/memory?token=${encodeURIComponent(secret)}`)
|
|
||||||
|
|
||||||
mihomoMemoryWs.onmessage = (e): void => {
|
mihomoMemoryWs.onmessage = (e): void => {
|
||||||
const data = e.data as string
|
const data = e.data as string
|
||||||
@ -280,15 +277,9 @@ export const stopMihomoLogs = (): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mihomoLogs = async (): Promise<void> => {
|
const mihomoLogs = async (): Promise<void> => {
|
||||||
const controledMihomoConfig = await getControledMihomoConfig()
|
const { 'log-level': logLevel = 'info' } = await getControledMihomoConfig()
|
||||||
const { secret = '', 'log-level': level = 'info' } = controledMihomoConfig
|
|
||||||
let { 'external-controller': server } = controledMihomoConfig
|
|
||||||
if (server?.startsWith(':')) server = `127.0.0.1${server}`
|
|
||||||
stopMihomoLogs()
|
|
||||||
|
|
||||||
mihomoLogsWs = new WebSocket(
|
mihomoLogsWs = new WebSocket(`ws+unix:${mihomoIpcPath}:/logs?level=${logLevel}`)
|
||||||
`ws://${server}/logs?token=${encodeURIComponent(secret)}&level=${level}`
|
|
||||||
)
|
|
||||||
|
|
||||||
mihomoLogsWs.onmessage = (e): void => {
|
mihomoLogsWs.onmessage = (e): void => {
|
||||||
const data = e.data as string
|
const data = e.data as string
|
||||||
@ -330,15 +321,7 @@ export const stopMihomoConnections = (): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mihomoConnections = async (): Promise<void> => {
|
const mihomoConnections = async (): Promise<void> => {
|
||||||
const controledMihomoConfig = await getControledMihomoConfig()
|
mihomoConnectionsWs = new WebSocket(`ws+unix:${mihomoIpcPath}:/connections`)
|
||||||
let server = controledMihomoConfig['external-controller']
|
|
||||||
const secret = controledMihomoConfig.secret ?? ''
|
|
||||||
if (server?.startsWith(':')) server = `127.0.0.1${server}`
|
|
||||||
stopMihomoConnections()
|
|
||||||
|
|
||||||
mihomoConnectionsWs = new WebSocket(
|
|
||||||
`ws://${server}/connections?token=${encodeURIComponent(secret)}`
|
|
||||||
)
|
|
||||||
|
|
||||||
mihomoConnectionsWs.onmessage = (e): void => {
|
mihomoConnectionsWs.onmessage = (e): void => {
|
||||||
const data = e.data as string
|
const data = e.data as string
|
||||||
|
|||||||
@ -5,13 +5,13 @@ import { getAppConfig } from '../config'
|
|||||||
export async function subStoreSubs(): Promise<ISubStoreSub[]> {
|
export async function subStoreSubs(): Promise<ISubStoreSub[]> {
|
||||||
const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
|
const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
|
||||||
const baseUrl = useCustomSubStore ? customSubStoreUrl : `http://127.0.0.1:${subStorePort}`
|
const baseUrl = useCustomSubStore ? customSubStoreUrl : `http://127.0.0.1:${subStorePort}`
|
||||||
const res = await axios.get(`${baseUrl}/api/subs`)
|
const res = await axios.get(`${baseUrl}/api/subs`, { responseType: 'json' })
|
||||||
return res.data.data as ISubStoreSub[]
|
return res.data.data as ISubStoreSub[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subStoreCollections(): Promise<ISubStoreSub[]> {
|
export async function subStoreCollections(): Promise<ISubStoreSub[]> {
|
||||||
const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
|
const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
|
||||||
const baseUrl = useCustomSubStore ? customSubStoreUrl : `http://127.0.0.1:${subStorePort}`
|
const baseUrl = useCustomSubStore ? customSubStoreUrl : `http://127.0.0.1:${subStorePort}`
|
||||||
const res = await axios.get(`${baseUrl}/api/collections`)
|
const res = await axios.get(`${baseUrl}/api/collections`, { responseType: 'json' })
|
||||||
return res.data.data as ISubStoreSub[]
|
return res.data.data as ISubStoreSub[]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,59 @@
|
|||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
import { registerIpcMainHandlers } from './utils/ipc'
|
import { registerIpcMainHandlers } from './utils/ipc'
|
||||||
import windowStateKeeper from 'electron-window-state'
|
import windowStateKeeper from 'electron-window-state'
|
||||||
import { app, shell, BrowserWindow, Menu, dialog, Notification } from 'electron'
|
import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron'
|
||||||
import { addProfileItem, getAppConfig } from './config'
|
import { addProfileItem, getAppConfig, patchAppConfig } from './config'
|
||||||
import { quitWithoutCore, startCore, stopCore } from './core/manager'
|
import { quitWithoutCore, startCore, stopCore } from './core/manager'
|
||||||
import { triggerSysProxy } from './sys/sysproxy'
|
import { triggerSysProxy } from './sys/sysproxy'
|
||||||
import icon from '../../resources/icon.png?asset'
|
import icon from '../../resources/icon.png?asset'
|
||||||
import { createTray } from './resolve/tray'
|
import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
|
||||||
import { init } from './utils/init'
|
import { init } from './utils/init'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { initShortcut } from './resolve/shortcut'
|
import { initShortcut } from './resolve/shortcut'
|
||||||
import { execSync } from 'child_process'
|
import { execSync, spawn, exec } from 'child_process'
|
||||||
import { createElevateTask } from './sys/misc'
|
import { createElevateTask } from './sys/misc'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import { stat } from 'fs/promises'
|
||||||
import { initProfileUpdater } from './core/profileUpdater'
|
import { initProfileUpdater } from './core/profileUpdater'
|
||||||
import { existsSync, writeFileSync } from 'fs'
|
import { existsSync, writeFileSync } from 'fs'
|
||||||
import { taskDir } from './utils/dirs'
|
import { exePath, taskDir } from './utils/dirs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { startMonitor } from './resolve/trafficMonitor'
|
||||||
|
import { showFloatingWindow } from './resolve/floatingWindow'
|
||||||
|
import iconv from 'iconv-lite'
|
||||||
|
import { initI18n } from '../shared/i18n'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
|
||||||
|
async function fixUserDataPermissions(): Promise<void> {
|
||||||
|
if (process.platform !== 'darwin') return
|
||||||
|
|
||||||
|
const userDataPath = app.getPath('userData')
|
||||||
|
if (!existsSync(userDataPath)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await stat(userDataPath)
|
||||||
|
const currentUid = process.getuid?.() || 0
|
||||||
|
|
||||||
|
if (stats.uid === 0 && currentUid !== 0) {
|
||||||
|
const execPromise = promisify(exec)
|
||||||
|
const username = process.env.USER || process.env.LOGNAME
|
||||||
|
if (username) {
|
||||||
|
await execPromise(`chown -R "${username}:staff" "${userDataPath}"`)
|
||||||
|
await execPromise(`chmod -R u+rwX "${userDataPath}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let quitTimeout: NodeJS.Timeout | null = null
|
let quitTimeout: NodeJS.Timeout | null = null
|
||||||
export let mainWindow: BrowserWindow | null = null
|
export let mainWindow: BrowserWindow | null = null
|
||||||
if (process.platform === 'win32' && !is.dev) {
|
|
||||||
|
if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin')) {
|
||||||
try {
|
try {
|
||||||
createElevateTask()
|
createElevateTask()
|
||||||
} catch (e) {
|
} catch (createError) {
|
||||||
try {
|
try {
|
||||||
if (process.argv.slice(1).length > 0) {
|
if (process.argv.slice(1).length > 0) {
|
||||||
writeFileSync(path.join(taskDir(), 'param.txt'), process.argv.slice(1).join(' '))
|
writeFileSync(path.join(taskDir(), 'param.txt'), process.argv.slice(1).join(' '))
|
||||||
@ -32,23 +63,67 @@ if (process.platform === 'win32' && !is.dev) {
|
|||||||
if (!existsSync(path.join(taskDir(), 'mihomo-party-run.exe'))) {
|
if (!existsSync(path.join(taskDir(), 'mihomo-party-run.exe'))) {
|
||||||
throw new Error('mihomo-party-run.exe not found')
|
throw new Error('mihomo-party-run.exe not found')
|
||||||
} else {
|
} else {
|
||||||
execSync('schtasks /run /tn mihomo-party-run')
|
execSync('%SystemRoot%\\System32\\schtasks.exe /run /tn mihomo-party-run')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dialog.showErrorBox('首次启动请以管理员权限运行', '首次启动请以管理员权限运行')
|
let createErrorStr = `${createError}`
|
||||||
|
let eStr = `${e}`
|
||||||
|
try {
|
||||||
|
createErrorStr = iconv.decode((createError as { stderr: Buffer }).stderr, 'gbk')
|
||||||
|
eStr = iconv.decode((e as { stderr: Buffer }).stderr, 'gbk')
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
dialog.showErrorBox(
|
||||||
|
i18next.t('common.error.adminRequired'),
|
||||||
|
`${i18next.t('common.error.adminRequired')}\n${createErrorStr}\n${eStr}`
|
||||||
|
)
|
||||||
} finally {
|
} finally {
|
||||||
app.exit()
|
app.exit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initApp(): Promise<void> {
|
||||||
|
await fixUserDataPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
initApp()
|
||||||
|
.then(() => {
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// ignore permission fix errors
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (!gotTheLock) {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function customRelaunch(): void {
|
||||||
|
const script = `while kill -0 ${process.pid} 2>/dev/null; do
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
${process.argv.join(' ')} & disown
|
||||||
|
exit
|
||||||
|
`
|
||||||
|
spawn('sh', ['-c', `"${script}"`], {
|
||||||
|
shell: true,
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
app.relaunch = customRelaunch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32' && !exePath().startsWith('C')) {
|
||||||
// https://github.com/electron/electron/issues/43278
|
// https://github.com/electron/electron/issues/43278
|
||||||
// https://github.com/electron/electron/issues/36698
|
// https://github.com/electron/electron/issues/36698
|
||||||
app.commandLine.appendSwitch('in-process-gpu')
|
app.commandLine.appendSwitch('in-process-gpu')
|
||||||
@ -68,32 +143,45 @@ app.on('open-url', async (_event, url) => {
|
|||||||
showMainWindow()
|
showMainWindow()
|
||||||
await handleDeepLink(url)
|
await handleDeepLink(url)
|
||||||
})
|
})
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
|
||||||
// for applications and their menu bar to stay active until the user quits
|
|
||||||
// explicitly with Cmd + Q.
|
|
||||||
app.on('window-all-closed', (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
// if (process.platform !== 'darwin') {
|
|
||||||
// app.quit()
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('before-quit', async () => {
|
app.on('before-quit', async (e) => {
|
||||||
await stopCore()
|
e.preventDefault()
|
||||||
triggerSysProxy(false)
|
triggerSysProxy(false)
|
||||||
|
await stopCore()
|
||||||
app.exit()
|
app.exit()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
powerMonitor.on('shutdown', async () => {
|
||||||
|
triggerSysProxy(false)
|
||||||
|
await stopCore()
|
||||||
|
app.exit()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取系统语言
|
||||||
|
function getSystemLanguage(): 'zh-CN' | 'en-US' {
|
||||||
|
const locale = app.getLocale()
|
||||||
|
return locale.startsWith('zh') ? 'zh-CN' : 'en-US'
|
||||||
|
}
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId('party.mihomo.app')
|
electronApp.setAppUserModelId('party.mihomo.app')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig()
|
||||||
|
// 如果配置中没有语言设置,则使用系统语言
|
||||||
|
if (!appConfig.language) {
|
||||||
|
const systemLanguage = getSystemLanguage()
|
||||||
|
await patchAppConfig({ language: systemLanguage })
|
||||||
|
appConfig.language = systemLanguage
|
||||||
|
}
|
||||||
|
await initI18n({ lng: appConfig.language })
|
||||||
await initPromise
|
await initPromise
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dialog.showErrorBox('应用初始化失败', `${e}`)
|
dialog.showErrorBox(i18next.t('common.error.initFailed'), `${e}`)
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -102,17 +190,29 @@ app.whenReady().then(async () => {
|
|||||||
await initProfileUpdater()
|
await initProfileUpdater()
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dialog.showErrorBox('内核启动出错', `${e}`)
|
dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await startMonitor()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
// Default open or close DevTools by F12 in development
|
// Default open or close DevTools by F12 in development
|
||||||
// and ignore CommandOrControl + R in production.
|
// and ignore CommandOrControl + R in production.
|
||||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window)
|
||||||
})
|
})
|
||||||
|
const { showFloatingWindow: showFloating = false, disableTray = false } = await getAppConfig()
|
||||||
registerIpcMainHandlers()
|
registerIpcMainHandlers()
|
||||||
await createWindow()
|
await createWindow()
|
||||||
|
if (showFloating) {
|
||||||
|
showFloatingWindow()
|
||||||
|
}
|
||||||
|
if (!disableTray) {
|
||||||
await createTray()
|
await createTray()
|
||||||
|
}
|
||||||
await initShortcut()
|
await initShortcut()
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
// On macOS it's common to re-create a window in the app when the
|
||||||
@ -131,7 +231,7 @@ async function handleDeepLink(url: string): Promise<void> {
|
|||||||
const profileUrl = urlObj.searchParams.get('url')
|
const profileUrl = urlObj.searchParams.get('url')
|
||||||
const profileName = urlObj.searchParams.get('name')
|
const profileName = urlObj.searchParams.get('name')
|
||||||
if (!profileUrl) {
|
if (!profileUrl) {
|
||||||
throw new Error('缺少参数 url')
|
throw new Error(i18next.t('profiles.error.urlParamMissing'))
|
||||||
}
|
}
|
||||||
await addProfileItem({
|
await addProfileItem({
|
||||||
type: 'remote',
|
type: 'remote',
|
||||||
@ -139,10 +239,10 @@ async function handleDeepLink(url: string): Promise<void> {
|
|||||||
url: profileUrl
|
url: profileUrl
|
||||||
})
|
})
|
||||||
mainWindow?.webContents.send('profileConfigUpdated')
|
mainWindow?.webContents.send('profileConfigUpdated')
|
||||||
new Notification({ title: '订阅导入成功' }).show()
|
new Notification({ title: i18next.t('profiles.notification.importSuccess') }).show()
|
||||||
break
|
break
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dialog.showErrorBox('订阅导入失败', `${url}\n${e}`)
|
dialog.showErrorBox(i18next.t('profiles.error.importFailed'), `${url}\n${e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,7 +252,8 @@ export async function createWindow(): Promise<void> {
|
|||||||
const { useWindowFrame = false } = await getAppConfig()
|
const { useWindowFrame = false } = await getAppConfig()
|
||||||
const mainWindowState = windowStateKeeper({
|
const mainWindowState = windowStateKeeper({
|
||||||
defaultWidth: 800,
|
defaultWidth: 800,
|
||||||
defaultHeight: 600
|
defaultHeight: 600,
|
||||||
|
file: 'window-state.json'
|
||||||
})
|
})
|
||||||
// https://github.com/electron/electron/issues/16521#issuecomment-582955104
|
// https://github.com/electron/electron/issues/16521#issuecomment-582955104
|
||||||
Menu.setApplicationMenu(null)
|
Menu.setApplicationMenu(null)
|
||||||
@ -177,7 +278,8 @@ export async function createWindow(): Promise<void> {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
sandbox: false
|
sandbox: false,
|
||||||
|
devTools: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
mainWindowState.manage(mainWindow)
|
mainWindowState.manage(mainWindow)
|
||||||
@ -207,10 +309,21 @@ export async function createWindow(): Promise<void> {
|
|||||||
mainWindow?.webContents.reload()
|
mainWindow?.webContents.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mainWindow.on('show', () => {
|
||||||
|
showDockIcon()
|
||||||
|
})
|
||||||
|
|
||||||
mainWindow.on('close', async (event) => {
|
mainWindow.on('close', async (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
mainWindow?.hide()
|
mainWindow?.hide()
|
||||||
const { autoQuitWithoutCore = false, autoQuitWithoutCoreDelay = 60 } = await getAppConfig()
|
const {
|
||||||
|
autoQuitWithoutCore = false,
|
||||||
|
autoQuitWithoutCoreDelay = 60,
|
||||||
|
useDockIcon = true
|
||||||
|
} = await getAppConfig()
|
||||||
|
if (!useDockIcon) {
|
||||||
|
hideDockIcon()
|
||||||
|
}
|
||||||
if (autoQuitWithoutCore) {
|
if (autoQuitWithoutCore) {
|
||||||
if (quitTimeout) {
|
if (quitTimeout) {
|
||||||
clearTimeout(quitTimeout)
|
clearTimeout(quitTimeout)
|
||||||
@ -221,11 +334,29 @@ export async function createWindow(): Promise<void> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mainWindow.on('resized', () => {
|
||||||
|
if (mainWindow) mainWindowState.saveState(mainWindow)
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.on('move', () => {
|
||||||
|
if (mainWindow) mainWindowState.saveState(mainWindow)
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.on('session-end', async () => {
|
||||||
|
triggerSysProxy(false)
|
||||||
|
await stopCore()
|
||||||
|
})
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url)
|
shell.openExternal(details.url)
|
||||||
return { action: 'deny' }
|
return { action: 'deny' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 在开发模式下自动打开 DevTools
|
||||||
|
if (is.dev) {
|
||||||
|
mainWindow.webContents.openDevTools()
|
||||||
|
}
|
||||||
|
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
// Load the remote URL for development or the local html file for production.
|
// Load the remote URL for development or the local html file for production.
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
@ -235,6 +366,14 @@ export async function createWindow(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function triggerMainWindow(): void {
|
||||||
|
if (mainWindow?.isVisible()) {
|
||||||
|
closeMainWindow()
|
||||||
|
} else {
|
||||||
|
showMainWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function showMainWindow(): void {
|
export function showMainWindow(): void {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
if (quitTimeout) {
|
if (quitTimeout) {
|
||||||
@ -244,3 +383,9 @@ export function showMainWindow(): void {
|
|||||||
mainWindow.focusOnWebView()
|
mainWindow.focusOnWebView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function closeMainWindow(): void {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import yaml from 'yaml'
|
|||||||
import { app, shell } from 'electron'
|
import { app, shell } from 'electron'
|
||||||
import { getControledMihomoConfig } from '../config'
|
import { getControledMihomoConfig } from '../config'
|
||||||
import { dataDir, exeDir, exePath, isPortable, resourcesFilesDir } from '../utils/dirs'
|
import { dataDir, exeDir, exePath, isPortable, resourcesFilesDir } from '../utils/dirs'
|
||||||
import { rm, writeFile } from 'fs/promises'
|
import { copyFile, rm, writeFile } from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import { exec, spawn } from 'child_process'
|
import { exec, execSync, spawn } from 'child_process'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
|
|
||||||
export async function checkUpdate(): Promise<IAppVersion | undefined> {
|
export async function checkUpdate(): Promise<IAppVersion | undefined> {
|
||||||
@ -20,7 +20,8 @@ export async function checkUpdate(): Promise<IAppVersion | undefined> {
|
|||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: mixedPort
|
port: mixedPort
|
||||||
}
|
},
|
||||||
|
responseType: 'text'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const latest = yaml.parse(res.data, { merge: true }) as IAppVersion
|
const latest = yaml.parse(res.data, { merge: true }) as IAppVersion
|
||||||
@ -39,8 +40,8 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
|
|||||||
'win32-x64': `mihomo-party-windows-${version}-x64-setup.exe`,
|
'win32-x64': `mihomo-party-windows-${version}-x64-setup.exe`,
|
||||||
'win32-ia32': `mihomo-party-windows-${version}-ia32-setup.exe`,
|
'win32-ia32': `mihomo-party-windows-${version}-ia32-setup.exe`,
|
||||||
'win32-arm64': `mihomo-party-windows-${version}-arm64-setup.exe`,
|
'win32-arm64': `mihomo-party-windows-${version}-arm64-setup.exe`,
|
||||||
'darwin-x64': `mihomo-party-macos-${version}-x64.dmg`,
|
'darwin-x64': `mihomo-party-macos-${version}-x64.pkg`,
|
||||||
'darwin-arm64': `mihomo-party-macos-${version}-arm64.dmg`
|
'darwin-arm64': `mihomo-party-macos-${version}-arm64.pkg`
|
||||||
}
|
}
|
||||||
let file = fileMap[`${process.platform}-${process.arch}`]
|
let file = fileMap[`${process.platform}-${process.arch}`]
|
||||||
if (isPortable()) {
|
if (isPortable()) {
|
||||||
@ -52,6 +53,14 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
|
|||||||
if (process.platform === 'win32' && parseInt(os.release()) < 10) {
|
if (process.platform === 'win32' && parseInt(os.release()) < 10) {
|
||||||
file = file.replace('windows', 'win7')
|
file = file.replace('windows', 'win7')
|
||||||
}
|
}
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const productVersion = execSync('sw_vers -productVersion', { encoding: 'utf8' })
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
if (parseInt(productVersion) < 11) {
|
||||||
|
file = file.replace('macos', 'catalina')
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (!existsSync(path.join(dataDir(), file))) {
|
if (!existsSync(path.join(dataDir(), file))) {
|
||||||
const res = await axios.get(`${baseUrl}${file}`, {
|
const res = await axios.get(`${baseUrl}${file}`, {
|
||||||
@ -74,9 +83,13 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
|
|||||||
}).unref()
|
}).unref()
|
||||||
}
|
}
|
||||||
if (file.endsWith('.7z')) {
|
if (file.endsWith('.7z')) {
|
||||||
|
await copyFile(path.join(resourcesFilesDir(), '7za.exe'), path.join(dataDir(), '7za.exe'))
|
||||||
spawn(
|
spawn(
|
||||||
path.join(resourcesFilesDir(), '7za.exe'),
|
'cmd',
|
||||||
['x', `-o"${exeDir()}"`, '-y', path.join(dataDir(), file)],
|
[
|
||||||
|
'/C',
|
||||||
|
`"timeout /t 2 /nobreak >nul && "${path.join(dataDir(), '7za.exe')}" x -o"${exeDir()}" -y "${path.join(dataDir(), file)}" & start "" "${exePath()}""`
|
||||||
|
],
|
||||||
{
|
{
|
||||||
shell: true,
|
shell: true,
|
||||||
detached: true
|
detached: true
|
||||||
@ -84,23 +97,12 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
|
|||||||
).unref()
|
).unref()
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
if (file.endsWith('.dmg')) {
|
if (file.endsWith('.pkg')) {
|
||||||
try {
|
try {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
const name = exePath().split('.app')[0].replace('/Applications/', '')
|
const shell = `installer -pkg ${path.join(dataDir(), file).replace(' ', '\\\\ ')} -target /`
|
||||||
await execPromise(
|
const command = `do shell script "${shell}" with administrator privileges`
|
||||||
`hdiutil attach "${path.join(dataDir(), file)}" -mountpoint "/Volumes/mihomo-party" -nobrowse`
|
await execPromise(`osascript -e '${command}'`)
|
||||||
)
|
|
||||||
try {
|
|
||||||
await execPromise(`mv /Applications/${name}.app /tmp`)
|
|
||||||
await execPromise('cp -R "/Volumes/mihomo-party/mihomo-party.app" /Applications/')
|
|
||||||
await execPromise(`rm -rf /tmp/${name}.app`)
|
|
||||||
} catch (e) {
|
|
||||||
await execPromise(`mv /tmp/${name}.app /Applications`)
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
await execPromise('hdiutil detach "/Volumes/mihomo-party"')
|
|
||||||
}
|
|
||||||
app.relaunch()
|
app.relaunch()
|
||||||
app.quit()
|
app.quit()
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -9,12 +9,19 @@ import {
|
|||||||
overrideDir,
|
overrideDir,
|
||||||
profileConfigPath,
|
profileConfigPath,
|
||||||
profilesDir,
|
profilesDir,
|
||||||
|
subStoreDir,
|
||||||
themesDir
|
themesDir
|
||||||
} from '../utils/dirs'
|
} from '../utils/dirs'
|
||||||
|
|
||||||
export async function webdavBackup(): Promise<boolean> {
|
export async function webdavBackup(): Promise<boolean> {
|
||||||
const { createClient } = await import('webdav/dist/node/index.js')
|
const { createClient } = await import('webdav/dist/node/index.js')
|
||||||
const { webdavUrl = '', webdavUsername = '', webdavPassword = '' } = await getAppConfig()
|
const {
|
||||||
|
webdavUrl = '',
|
||||||
|
webdavUsername = '',
|
||||||
|
webdavPassword = '',
|
||||||
|
webdavDir = 'mihomo-party',
|
||||||
|
webdavMaxBackups = 0
|
||||||
|
} = await getAppConfig()
|
||||||
const zip = new AdmZip()
|
const zip = new AdmZip()
|
||||||
|
|
||||||
zip.addLocalFile(appConfigPath())
|
zip.addLocalFile(appConfigPath())
|
||||||
@ -24,7 +31,7 @@ export async function webdavBackup(): Promise<boolean> {
|
|||||||
zip.addLocalFolder(themesDir(), 'themes')
|
zip.addLocalFolder(themesDir(), 'themes')
|
||||||
zip.addLocalFolder(profilesDir(), 'profiles')
|
zip.addLocalFolder(profilesDir(), 'profiles')
|
||||||
zip.addLocalFolder(overrideDir(), 'override')
|
zip.addLocalFolder(overrideDir(), 'override')
|
||||||
zip.addLocalFolder(overrideDir(), 'substore')
|
zip.addLocalFolder(subStoreDir(), 'substore')
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
||||||
|
|
||||||
@ -33,36 +40,80 @@ export async function webdavBackup(): Promise<boolean> {
|
|||||||
password: webdavPassword
|
password: webdavPassword
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
await client.createDirectory('mihomo-party')
|
await client.createDirectory(webdavDir)
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
return await client.putFileContents(`mihomo-party/${zipFileName}`, zip.toBuffer())
|
const result = await client.putFileContents(`${webdavDir}/${zipFileName}`, zip.toBuffer())
|
||||||
|
|
||||||
|
if (webdavMaxBackups > 0) {
|
||||||
|
try {
|
||||||
|
const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
|
||||||
|
const fileList = Array.isArray(files) ? files : files.data
|
||||||
|
|
||||||
|
const currentPlatformFiles = fileList.filter((file) => {
|
||||||
|
return file.basename.startsWith(`${process.platform}_`)
|
||||||
|
})
|
||||||
|
|
||||||
|
currentPlatformFiles.sort((a, b) => {
|
||||||
|
const timeA = a.basename.match(/_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.zip$/)?.[1] || ''
|
||||||
|
const timeB = b.basename.match(/_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.zip$/)?.[1] || ''
|
||||||
|
return timeB.localeCompare(timeA)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (currentPlatformFiles.length > webdavMaxBackups) {
|
||||||
|
const filesToDelete = currentPlatformFiles.slice(webdavMaxBackups)
|
||||||
|
|
||||||
|
for (let i = 0; i < filesToDelete.length; i++) {
|
||||||
|
const file = filesToDelete[i]
|
||||||
|
await client.deleteFile(`${webdavDir}/${file.basename}`)
|
||||||
|
|
||||||
|
if (i < filesToDelete.length - 1) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clean up old backup files:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function webdavRestore(filename: string): Promise<void> {
|
export async function webdavRestore(filename: string): Promise<void> {
|
||||||
const { createClient } = await import('webdav/dist/node/index.js')
|
const { createClient } = await import('webdav/dist/node/index.js')
|
||||||
const { webdavUrl = '', webdavUsername = '', webdavPassword = '' } = await getAppConfig()
|
const {
|
||||||
|
webdavUrl = '',
|
||||||
|
webdavUsername = '',
|
||||||
|
webdavPassword = '',
|
||||||
|
webdavDir = 'mihomo-party'
|
||||||
|
} = await getAppConfig()
|
||||||
|
|
||||||
const client = createClient(webdavUrl, {
|
const client = createClient(webdavUrl, {
|
||||||
username: webdavUsername,
|
username: webdavUsername,
|
||||||
password: webdavPassword
|
password: webdavPassword
|
||||||
})
|
})
|
||||||
const zipData = await client.getFileContents(`mihomo-party/${filename}`)
|
const zipData = await client.getFileContents(`${webdavDir}/${filename}`)
|
||||||
const zip = new AdmZip(zipData as Buffer)
|
const zip = new AdmZip(zipData as Buffer)
|
||||||
zip.extractAllTo(dataDir(), true)
|
zip.extractAllTo(dataDir(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listWebdavBackups(): Promise<string[]> {
|
export async function listWebdavBackups(): Promise<string[]> {
|
||||||
const { createClient } = await import('webdav/dist/node/index.js')
|
const { createClient } = await import('webdav/dist/node/index.js')
|
||||||
const { webdavUrl = '', webdavUsername = '', webdavPassword = '' } = await getAppConfig()
|
const {
|
||||||
|
webdavUrl = '',
|
||||||
|
webdavUsername = '',
|
||||||
|
webdavPassword = '',
|
||||||
|
webdavDir = 'mihomo-party'
|
||||||
|
} = await getAppConfig()
|
||||||
|
|
||||||
const client = createClient(webdavUrl, {
|
const client = createClient(webdavUrl, {
|
||||||
username: webdavUsername,
|
username: webdavUsername,
|
||||||
password: webdavPassword
|
password: webdavPassword
|
||||||
})
|
})
|
||||||
const files = await client.getDirectoryContents('mihomo-party', { glob: '*.zip' })
|
const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
|
||||||
if (Array.isArray(files)) {
|
if (Array.isArray(files)) {
|
||||||
return files.map((file) => file.basename)
|
return files.map((file) => file.basename)
|
||||||
} else {
|
} else {
|
||||||
@ -72,11 +123,16 @@ export async function listWebdavBackups(): Promise<string[]> {
|
|||||||
|
|
||||||
export async function webdavDelete(filename: string): Promise<void> {
|
export async function webdavDelete(filename: string): Promise<void> {
|
||||||
const { createClient } = await import('webdav/dist/node/index.js')
|
const { createClient } = await import('webdav/dist/node/index.js')
|
||||||
const { webdavUrl = '', webdavUsername = '', webdavPassword = '' } = await getAppConfig()
|
const {
|
||||||
|
webdavUrl = '',
|
||||||
|
webdavUsername = '',
|
||||||
|
webdavPassword = '',
|
||||||
|
webdavDir = 'mihomo-party'
|
||||||
|
} = await getAppConfig()
|
||||||
|
|
||||||
const client = createClient(webdavUrl, {
|
const client = createClient(webdavUrl, {
|
||||||
username: webdavUsername,
|
username: webdavUsername,
|
||||||
password: webdavPassword
|
password: webdavPassword
|
||||||
})
|
})
|
||||||
await client.deleteFile(`mihomo-party/${filename}`)
|
await client.deleteFile(`${webdavDir}/${filename}`)
|
||||||
}
|
}
|
||||||
|
|||||||
90
src/main/resolve/floatingWindow.ts
Normal file
90
src/main/resolve/floatingWindow.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { is } from '@electron-toolkit/utils'
|
||||||
|
import { BrowserWindow, ipcMain } from 'electron'
|
||||||
|
import windowStateKeeper from 'electron-window-state'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { getAppConfig, patchAppConfig } from '../config'
|
||||||
|
import { applyTheme } from './theme'
|
||||||
|
import { buildContextMenu, showTrayIcon } from './tray'
|
||||||
|
|
||||||
|
export let floatingWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
|
async function createFloatingWindow(): Promise<void> {
|
||||||
|
const floatingWindowState = windowStateKeeper({
|
||||||
|
file: 'floating-window-state.json'
|
||||||
|
})
|
||||||
|
const { customTheme = 'default.css' } = await getAppConfig()
|
||||||
|
floatingWindow = new BrowserWindow({
|
||||||
|
width: 120,
|
||||||
|
height: 42,
|
||||||
|
x: floatingWindowState.x,
|
||||||
|
y: floatingWindowState.y,
|
||||||
|
show: false,
|
||||||
|
frame: false,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
resizable: false,
|
||||||
|
transparent: true,
|
||||||
|
skipTaskbar: true,
|
||||||
|
minimizable: false,
|
||||||
|
maximizable: false,
|
||||||
|
fullscreenable: false,
|
||||||
|
closable: false,
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
spellcheck: false,
|
||||||
|
sandbox: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
floatingWindowState.manage(floatingWindow)
|
||||||
|
floatingWindow.on('ready-to-show', () => {
|
||||||
|
applyTheme(customTheme)
|
||||||
|
floatingWindow?.show()
|
||||||
|
floatingWindow?.setAlwaysOnTop(true, 'screen-saver')
|
||||||
|
})
|
||||||
|
floatingWindow.on('moved', () => {
|
||||||
|
if (floatingWindow) floatingWindowState.saveState(floatingWindow)
|
||||||
|
})
|
||||||
|
ipcMain.on('updateFloatingWindow', () => {
|
||||||
|
if (floatingWindow) {
|
||||||
|
floatingWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
floatingWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/floating.html`)
|
||||||
|
} else {
|
||||||
|
floatingWindow.loadFile(join(__dirname, '../renderer/floating.html'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showFloatingWindow(): Promise<void> {
|
||||||
|
if (floatingWindow) {
|
||||||
|
floatingWindow.show()
|
||||||
|
} else {
|
||||||
|
createFloatingWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function triggerFloatingWindow(): Promise<void> {
|
||||||
|
if (floatingWindow?.isVisible()) {
|
||||||
|
await patchAppConfig({ showFloatingWindow: false })
|
||||||
|
await closeFloatingWindow()
|
||||||
|
} else {
|
||||||
|
await patchAppConfig({ showFloatingWindow: true })
|
||||||
|
await showFloatingWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeFloatingWindow(): Promise<void> {
|
||||||
|
if (floatingWindow) {
|
||||||
|
floatingWindow.close()
|
||||||
|
floatingWindow.destroy()
|
||||||
|
floatingWindow = null
|
||||||
|
}
|
||||||
|
await showTrayIcon()
|
||||||
|
await patchAppConfig({ disableTray: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showContextMenu(): Promise<void> {
|
||||||
|
const menu = await buildContextMenu()
|
||||||
|
menu.popup()
|
||||||
|
}
|
||||||
@ -20,7 +20,8 @@ async function listGists(token: string): Promise<GistInfo[]> {
|
|||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port
|
port
|
||||||
}
|
},
|
||||||
|
responseType: 'json'
|
||||||
})
|
})
|
||||||
return res.data as GistInfo[]
|
return res.data as GistInfo[]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
import { getAppConfig, getControledMihomoConfig } from '../config'
|
import { getAppConfig, getControledMihomoConfig } from '../config'
|
||||||
import { Worker } from 'worker_threads'
|
import { Worker } from 'worker_threads'
|
||||||
import { mihomoWorkDir, resourcesFilesDir, subStoreDir } from '../utils/dirs'
|
import { dataDir, mihomoWorkDir, subStoreDir, substoreLogPath } from '../utils/dirs'
|
||||||
import subStoreIcon from '../../../resources/subStoreIcon.png?asset'
|
import subStoreIcon from '../../../resources/subStoreIcon.png?asset'
|
||||||
|
import { createWriteStream, existsSync, mkdirSync } from 'fs'
|
||||||
|
import { writeFile, rm, cp } from 'fs/promises'
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
import net from 'net'
|
import net from 'net'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { nativeImage } from 'electron'
|
import { nativeImage } from 'electron'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import axios from 'axios'
|
||||||
|
import AdmZip from 'adm-zip'
|
||||||
|
|
||||||
export let pacPort: number
|
export let pacPort: number
|
||||||
export let subStorePort: number
|
export let subStorePort: number
|
||||||
export let subStoreFrontendPort: number
|
export let subStoreFrontendPort: number
|
||||||
|
let subStoreFrontendServer: http.Server
|
||||||
|
let subStoreBackendWorker: Worker
|
||||||
|
|
||||||
const defaultPacScript = `
|
const defaultPacScript = `
|
||||||
function FindProxyForURL(url, host) {
|
function FindProxyForURL(url, host) {
|
||||||
@ -21,7 +27,6 @@ function FindProxyForURL(url, host) {
|
|||||||
export function findAvailablePort(startPort: number): Promise<number> {
|
export function findAvailablePort(startPort: number): Promise<number> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = net.createServer()
|
const server = net.createServer()
|
||||||
server.unref()
|
|
||||||
server.on('error', (err) => {
|
server.on('error', (err) => {
|
||||||
if (startPort <= 65535) {
|
if (startPort <= 65535) {
|
||||||
resolve(findAvailablePort(startPort + 1))
|
resolve(findAvailablePort(startPort + 1))
|
||||||
@ -29,55 +34,81 @@ export function findAvailablePort(startPort: number): Promise<number> {
|
|||||||
reject(err)
|
reject(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
server.on('listening', () => {
|
||||||
server.listen(startPort, () => {
|
|
||||||
// 端口可用
|
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
resolve(startPort)
|
resolve(startPort)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
server.listen(startPort, '127.0.0.1')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pacServer: http.Server
|
||||||
|
|
||||||
export async function startPacServer(): Promise<void> {
|
export async function startPacServer(): Promise<void> {
|
||||||
pacPort = await findAvailablePort(10000)
|
await stopPacServer()
|
||||||
const server = http
|
const { sysProxy } = await getAppConfig()
|
||||||
.createServer(async (_req, res) => {
|
const { mode = 'manual', host: cHost, pacScript } = sysProxy
|
||||||
const {
|
if (mode !== 'auto') {
|
||||||
sysProxy: { pacScript }
|
return
|
||||||
} = await getAppConfig()
|
}
|
||||||
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
|
const host = cHost || '127.0.0.1'
|
||||||
let script = pacScript || defaultPacScript
|
let script = pacScript || defaultPacScript
|
||||||
|
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
|
||||||
script = script.replaceAll('%mixed-port%', port.toString())
|
script = script.replaceAll('%mixed-port%', port.toString())
|
||||||
|
pacPort = await findAvailablePort(10000)
|
||||||
|
pacServer = http
|
||||||
|
.createServer(async (_req, res) => {
|
||||||
res.writeHead(200, { 'Content-Type': 'application/x-ns-proxy-autoconfig' })
|
res.writeHead(200, { 'Content-Type': 'application/x-ns-proxy-autoconfig' })
|
||||||
res.end(script)
|
res.end(script)
|
||||||
})
|
})
|
||||||
.listen(pacPort)
|
.listen(pacPort, host)
|
||||||
server.unref()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startSubStoreServer(): Promise<void> {
|
export async function stopPacServer(): Promise<void> {
|
||||||
|
if (pacServer) {
|
||||||
|
pacServer.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startSubStoreFrontendServer(): Promise<void> {
|
||||||
|
const { useSubStore = true, subStoreHost = '127.0.0.1' } = await getAppConfig()
|
||||||
|
if (!useSubStore) return
|
||||||
|
await stopSubStoreFrontendServer()
|
||||||
|
subStoreFrontendPort = await findAvailablePort(14122)
|
||||||
|
const app = express()
|
||||||
|
app.use(express.static(path.join(mihomoWorkDir(), 'sub-store-frontend')))
|
||||||
|
subStoreFrontendServer = app.listen(subStoreFrontendPort, subStoreHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopSubStoreFrontendServer(): Promise<void> {
|
||||||
|
if (subStoreFrontendServer) {
|
||||||
|
subStoreFrontendServer.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startSubStoreBackendServer(): Promise<void> {
|
||||||
const {
|
const {
|
||||||
useSubStore = true,
|
useSubStore = true,
|
||||||
useCustomSubStore = false,
|
useCustomSubStore = false,
|
||||||
|
useProxyInSubStore = false,
|
||||||
|
subStoreHost = '127.0.0.1',
|
||||||
subStoreBackendSyncCron = '',
|
subStoreBackendSyncCron = '',
|
||||||
subStoreBackendDownloadCron = '',
|
subStoreBackendDownloadCron = '',
|
||||||
subStoreBackendUploadCron = ''
|
subStoreBackendUploadCron = ''
|
||||||
} = await getAppConfig()
|
} = await getAppConfig()
|
||||||
|
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
|
||||||
if (!useSubStore) return
|
if (!useSubStore) return
|
||||||
if (!subStoreFrontendPort) {
|
if (!useCustomSubStore) {
|
||||||
subStoreFrontendPort = await findAvailablePort(4000)
|
await stopSubStoreBackendServer()
|
||||||
const app = express()
|
subStorePort = await findAvailablePort(38324)
|
||||||
app.use(express.static(path.join(resourcesFilesDir(), 'sub-store-frontend')))
|
|
||||||
app.listen(subStoreFrontendPort)
|
|
||||||
}
|
|
||||||
if (!useCustomSubStore && !subStorePort) {
|
|
||||||
subStorePort = await findAvailablePort(3000)
|
|
||||||
const icon = nativeImage.createFromPath(subStoreIcon)
|
const icon = nativeImage.createFromPath(subStoreIcon)
|
||||||
icon.toDataURL()
|
icon.toDataURL()
|
||||||
new Worker(path.join(resourcesFilesDir(), 'sub-store.bundle.js'), {
|
const stdout = createWriteStream(substoreLogPath(), { flags: 'a' })
|
||||||
env: {
|
const stderr = createWriteStream(substoreLogPath(), { flags: 'a' })
|
||||||
|
const env = {
|
||||||
SUB_STORE_BACKEND_API_PORT: subStorePort.toString(),
|
SUB_STORE_BACKEND_API_PORT: subStorePort.toString(),
|
||||||
|
SUB_STORE_BACKEND_API_HOST: subStoreHost,
|
||||||
SUB_STORE_DATA_BASE_PATH: subStoreDir(),
|
SUB_STORE_DATA_BASE_PATH: subStoreDir(),
|
||||||
SUB_STORE_BACKEND_CUSTOM_ICON: icon.toDataURL(),
|
SUB_STORE_BACKEND_CUSTOM_ICON: icon.toDataURL(),
|
||||||
SUB_STORE_BACKEND_CUSTOM_NAME: 'Mihomo Party',
|
SUB_STORE_BACKEND_CUSTOM_NAME: 'Mihomo Party',
|
||||||
@ -87,6 +118,81 @@ export async function startSubStoreServer(): Promise<void> {
|
|||||||
SUB_STORE_MMDB_COUNTRY_PATH: path.join(mihomoWorkDir(), 'country.mmdb'),
|
SUB_STORE_MMDB_COUNTRY_PATH: path.join(mihomoWorkDir(), 'country.mmdb'),
|
||||||
SUB_STORE_MMDB_ASN_PATH: path.join(mihomoWorkDir(), 'ASN.mmdb')
|
SUB_STORE_MMDB_ASN_PATH: path.join(mihomoWorkDir(), 'ASN.mmdb')
|
||||||
}
|
}
|
||||||
|
subStoreBackendWorker = new Worker(path.join(mihomoWorkDir(), 'sub-store.bundle.js'), {
|
||||||
|
env: useProxyInSubStore
|
||||||
|
? {
|
||||||
|
...env,
|
||||||
|
HTTP_PROXY: `http://127.0.0.1:${port}`,
|
||||||
|
HTTPS_PROXY: `http://127.0.0.1:${port}`,
|
||||||
|
ALL_PROXY: `http://127.0.0.1:${port}`
|
||||||
|
}
|
||||||
|
: env
|
||||||
})
|
})
|
||||||
|
subStoreBackendWorker.stdout.pipe(stdout)
|
||||||
|
subStoreBackendWorker.stderr.pipe(stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopSubStoreBackendServer(): Promise<void> {
|
||||||
|
if (subStoreBackendWorker) {
|
||||||
|
subStoreBackendWorker.terminate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadSubStore(): Promise<void> {
|
||||||
|
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
|
||||||
|
const frontendDir = path.join(mihomoWorkDir(), 'sub-store-frontend')
|
||||||
|
const backendPath = path.join(mihomoWorkDir(), 'sub-store.bundle.js')
|
||||||
|
const tempDir = path.join(dataDir(), 'temp')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建临时目录
|
||||||
|
if (existsSync(tempDir)) {
|
||||||
|
await rm(tempDir, { recursive: true })
|
||||||
|
}
|
||||||
|
mkdirSync(tempDir, { recursive: true })
|
||||||
|
|
||||||
|
// 下载后端文件
|
||||||
|
const tempBackendPath = path.join(tempDir, 'sub-store.bundle.js')
|
||||||
|
const backendRes = await axios.get(
|
||||||
|
'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js',
|
||||||
|
{
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
headers: { 'Content-Type': 'application/octet-stream' },
|
||||||
|
proxy: {
|
||||||
|
protocol: 'http',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: mixedPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await writeFile(tempBackendPath, Buffer.from(backendRes.data))
|
||||||
|
// 下载前端文件
|
||||||
|
const tempFrontendDir = path.join(tempDir, 'dist')
|
||||||
|
const frontendRes = await axios.get(
|
||||||
|
'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip',
|
||||||
|
{
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
headers: { 'Content-Type': 'application/octet-stream' },
|
||||||
|
proxy: {
|
||||||
|
protocol: 'http',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: mixedPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// 先解压到临时目录
|
||||||
|
const zip = new AdmZip(Buffer.from(frontendRes.data))
|
||||||
|
zip.extractAllTo(tempDir, true)
|
||||||
|
await cp(tempBackendPath, backendPath)
|
||||||
|
if (existsSync(frontendDir)) {
|
||||||
|
await rm(frontendDir, { recursive: true })
|
||||||
|
}
|
||||||
|
mkdirSync(frontendDir, { recursive: true })
|
||||||
|
await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true })
|
||||||
|
await rm(tempDir, { recursive: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('substore.downloadFailed:', error)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { app, globalShortcut, ipcMain, Notification } from 'electron'
|
import { app, globalShortcut, ipcMain, Notification } from 'electron'
|
||||||
import { mainWindow, showMainWindow } from '..'
|
import { mainWindow, triggerMainWindow } from '..'
|
||||||
import {
|
import {
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
getControledMihomoConfig,
|
getControledMihomoConfig,
|
||||||
@ -9,6 +9,8 @@ import {
|
|||||||
import { triggerSysProxy } from '../sys/sysproxy'
|
import { triggerSysProxy } from '../sys/sysproxy'
|
||||||
import { patchMihomoConfig } from '../core/mihomoApi'
|
import { patchMihomoConfig } from '../core/mihomoApi'
|
||||||
import { quitWithoutCore, restartCore } from '../core/manager'
|
import { quitWithoutCore, restartCore } from '../core/manager'
|
||||||
|
import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
|
||||||
|
import i18next from '../../shared/i18n'
|
||||||
|
|
||||||
export async function registerShortcut(
|
export async function registerShortcut(
|
||||||
oldShortcut: string,
|
oldShortcut: string,
|
||||||
@ -24,11 +26,12 @@ export async function registerShortcut(
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case 'showWindowShortcut': {
|
case 'showWindowShortcut': {
|
||||||
return globalShortcut.register(newShortcut, () => {
|
return globalShortcut.register(newShortcut, () => {
|
||||||
if (mainWindow?.isVisible()) {
|
triggerMainWindow()
|
||||||
mainWindow?.close()
|
})
|
||||||
} else {
|
|
||||||
showMainWindow()
|
|
||||||
}
|
}
|
||||||
|
case 'showFloatingWindowShortcut': {
|
||||||
|
return globalShortcut.register(newShortcut, async () => {
|
||||||
|
await triggerFloatingWindow()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case 'triggerSysProxyShortcut': {
|
case 'triggerSysProxyShortcut': {
|
||||||
@ -40,12 +43,13 @@ export async function registerShortcut(
|
|||||||
await triggerSysProxy(!enable)
|
await triggerSysProxy(!enable)
|
||||||
await patchAppConfig({ sysProxy: { enable: !enable } })
|
await patchAppConfig({ sysProxy: { enable: !enable } })
|
||||||
new Notification({
|
new Notification({
|
||||||
title: `系统代理已${!enable ? '开启' : '关闭'}`
|
title: i18next.t(!enable ? 'common.notification.systemProxyEnabled' : 'common.notification.systemProxyDisabled')
|
||||||
}).show()
|
}).show()
|
||||||
|
mainWindow?.webContents.send('appConfigUpdated')
|
||||||
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
mainWindow?.webContents.send('appConfigUpdated')
|
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -62,12 +66,13 @@ export async function registerShortcut(
|
|||||||
}
|
}
|
||||||
await restartCore()
|
await restartCore()
|
||||||
new Notification({
|
new Notification({
|
||||||
title: `虚拟网卡已${!enable ? '开启' : '关闭'}`
|
title: i18next.t(!enable ? 'common.notification.tunEnabled' : 'common.notification.tunDisabled')
|
||||||
}).show()
|
}).show()
|
||||||
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -77,7 +82,7 @@ export async function registerShortcut(
|
|||||||
await patchControledMihomoConfig({ mode: 'rule' })
|
await patchControledMihomoConfig({ mode: 'rule' })
|
||||||
await patchMihomoConfig({ mode: 'rule' })
|
await patchMihomoConfig({ mode: 'rule' })
|
||||||
new Notification({
|
new Notification({
|
||||||
title: '已切换至规则模式'
|
title: i18next.t('common.notification.ruleMode')
|
||||||
}).show()
|
}).show()
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
@ -88,7 +93,7 @@ export async function registerShortcut(
|
|||||||
await patchControledMihomoConfig({ mode: 'global' })
|
await patchControledMihomoConfig({ mode: 'global' })
|
||||||
await patchMihomoConfig({ mode: 'global' })
|
await patchMihomoConfig({ mode: 'global' })
|
||||||
new Notification({
|
new Notification({
|
||||||
title: '已切换至全局模式'
|
title: i18next.t('common.notification.globalMode')
|
||||||
}).show()
|
}).show()
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
@ -99,7 +104,7 @@ export async function registerShortcut(
|
|||||||
await patchControledMihomoConfig({ mode: 'direct' })
|
await patchControledMihomoConfig({ mode: 'direct' })
|
||||||
await patchMihomoConfig({ mode: 'direct' })
|
await patchMihomoConfig({ mode: 'direct' })
|
||||||
new Notification({
|
new Notification({
|
||||||
title: '已切换至直连模式'
|
title: i18next.t('common.notification.directMode')
|
||||||
}).show()
|
}).show()
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
@ -122,6 +127,7 @@ export async function registerShortcut(
|
|||||||
|
|
||||||
export async function initShortcut(): Promise<void> {
|
export async function initShortcut(): Promise<void> {
|
||||||
const {
|
const {
|
||||||
|
showFloatingWindowShortcut,
|
||||||
showWindowShortcut,
|
showWindowShortcut,
|
||||||
triggerSysProxyShortcut,
|
triggerSysProxyShortcut,
|
||||||
triggerTunShortcut,
|
triggerTunShortcut,
|
||||||
@ -138,6 +144,13 @@ export async function initShortcut(): Promise<void> {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showFloatingWindowShortcut) {
|
||||||
|
try {
|
||||||
|
await registerShortcut('', showFloatingWindowShortcut, 'showFloatingWindowShortcut')
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
if (triggerSysProxyShortcut) {
|
if (triggerSysProxyShortcut) {
|
||||||
try {
|
try {
|
||||||
await registerShortcut('', triggerSysProxyShortcut, 'triggerSysProxyShortcut')
|
await registerShortcut('', triggerSysProxyShortcut, 'triggerSysProxyShortcut')
|
||||||
|
|||||||
@ -6,8 +6,11 @@ import AdmZip from 'adm-zip'
|
|||||||
import { getControledMihomoConfig } from '../config'
|
import { getControledMihomoConfig } from '../config'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import { mainWindow } from '..'
|
import { mainWindow } from '..'
|
||||||
|
import { floatingWindow } from './floatingWindow'
|
||||||
|
import { t } from 'i18next'
|
||||||
|
|
||||||
let insertedCSSKey: string | undefined = undefined
|
let insertedCSSKeyMain: string | undefined = undefined
|
||||||
|
let insertedCSSKeyFloating: string | undefined = undefined
|
||||||
|
|
||||||
export async function resolveThemes(): Promise<{ key: string; label: string }[]> {
|
export async function resolveThemes(): Promise<{ key: string; label: string }[]> {
|
||||||
const files = await readdir(themesDir())
|
const files = await readdir(themesDir())
|
||||||
@ -26,7 +29,7 @@ export async function resolveThemes(): Promise<{ key: string; label: string }[]>
|
|||||||
if (themes.find((theme) => theme.key === 'default.css')) {
|
if (themes.find((theme) => theme.key === 'default.css')) {
|
||||||
return themes
|
return themes
|
||||||
} else {
|
} else {
|
||||||
return [{ key: 'default.css', label: '默认' }, ...themes]
|
return [{ key: 'default.css', label: t('common.default') }, ...themes]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +70,12 @@ export async function writeTheme(theme: string, css: string): Promise<void> {
|
|||||||
|
|
||||||
export async function applyTheme(theme: string): Promise<void> {
|
export async function applyTheme(theme: string): Promise<void> {
|
||||||
const css = await readTheme(theme)
|
const css = await readTheme(theme)
|
||||||
await mainWindow?.webContents.removeInsertedCSS(insertedCSSKey || '')
|
await mainWindow?.webContents.removeInsertedCSS(insertedCSSKeyMain || '')
|
||||||
insertedCSSKey = await mainWindow?.webContents.insertCSS(css)
|
insertedCSSKeyMain = await mainWindow?.webContents.insertCSS(css)
|
||||||
|
try {
|
||||||
|
await floatingWindow?.webContents.removeInsertedCSS(insertedCSSKeyFloating || '')
|
||||||
|
insertedCSSKeyFloating = await floatingWindow?.webContents.insertCSS(css)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/main/resolve/trafficMonitor.ts
Normal file
42
src/main/resolve/trafficMonitor.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { ChildProcess, spawn } from 'child_process'
|
||||||
|
import { getAppConfig } from '../config'
|
||||||
|
import { dataDir, resourcesFilesDir } from '../utils/dirs'
|
||||||
|
import path from 'path'
|
||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { readFile, rm, writeFile } from 'fs/promises'
|
||||||
|
|
||||||
|
let child: ChildProcess
|
||||||
|
|
||||||
|
export async function startMonitor(detached = false): Promise<void> {
|
||||||
|
if (process.platform !== 'win32') return
|
||||||
|
if (existsSync(path.join(dataDir(), 'monitor.pid'))) {
|
||||||
|
const pid = parseInt(await readFile(path.join(dataDir(), 'monitor.pid'), 'utf-8'))
|
||||||
|
try {
|
||||||
|
process.kill(pid, 'SIGINT')
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
await rm(path.join(dataDir(), 'monitor.pid'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await stopMonitor()
|
||||||
|
const { showTraffic = false } = await getAppConfig()
|
||||||
|
if (!showTraffic) return
|
||||||
|
child = spawn(path.join(resourcesFilesDir(), 'TrafficMonitor/TrafficMonitor.exe'), [], {
|
||||||
|
cwd: path.join(resourcesFilesDir(), 'TrafficMonitor'),
|
||||||
|
detached: detached,
|
||||||
|
stdio: detached ? 'ignore' : undefined
|
||||||
|
})
|
||||||
|
if (detached) {
|
||||||
|
if (child && child.pid) {
|
||||||
|
await writeFile(path.join(dataDir(), 'monitor.pid'), child.pid.toString())
|
||||||
|
}
|
||||||
|
child.unref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopMonitor(): Promise<void> {
|
||||||
|
if (child) {
|
||||||
|
child.kill('SIGINT')
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,15 +15,22 @@ import {
|
|||||||
mihomoGroups,
|
mihomoGroups,
|
||||||
patchMihomoConfig
|
patchMihomoConfig
|
||||||
} from '../core/mihomoApi'
|
} from '../core/mihomoApi'
|
||||||
import { mainWindow, showMainWindow } from '..'
|
import { mainWindow, showMainWindow, triggerMainWindow } from '..'
|
||||||
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
|
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
|
||||||
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
|
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
|
||||||
import { triggerSysProxy } from '../sys/sysproxy'
|
import { triggerSysProxy } from '../sys/sysproxy'
|
||||||
import { quitWithoutCore, restartCore } from '../core/manager'
|
import { quitWithoutCore, restartCore } from '../core/manager'
|
||||||
|
import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
|
||||||
|
import { t } from 'i18next'
|
||||||
|
|
||||||
export let tray: Tray | null = null
|
export let tray: Tray | null = null
|
||||||
|
|
||||||
const buildContextMenu = async (): Promise<Menu> => {
|
export const buildContextMenu = async (): Promise<Menu> => {
|
||||||
|
// 添加调试日志
|
||||||
|
console.log('Current translation for tray.showWindow:', t('tray.showWindow'))
|
||||||
|
console.log('Current translation for tray.hideFloatingWindow:', t('tray.hideFloatingWindow'))
|
||||||
|
console.log('Current translation for tray.showFloatingWindow:', t('tray.showFloatingWindow'))
|
||||||
|
|
||||||
const { mode, tun } = await getControledMihomoConfig()
|
const { mode, tun } = await getControledMihomoConfig()
|
||||||
const {
|
const {
|
||||||
sysProxy,
|
sysProxy,
|
||||||
@ -31,6 +38,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
autoCloseConnection,
|
autoCloseConnection,
|
||||||
proxyInTray = true,
|
proxyInTray = true,
|
||||||
triggerSysProxyShortcut = '',
|
triggerSysProxyShortcut = '',
|
||||||
|
showFloatingWindowShortcut = '',
|
||||||
showWindowShortcut = '',
|
showWindowShortcut = '',
|
||||||
triggerTunShortcut = '',
|
triggerTunShortcut = '',
|
||||||
ruleModeShortcut = '',
|
ruleModeShortcut = '',
|
||||||
@ -84,15 +92,24 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
{
|
{
|
||||||
id: 'show',
|
id: 'show',
|
||||||
accelerator: showWindowShortcut,
|
accelerator: showWindowShortcut,
|
||||||
label: '显示窗口',
|
label: t('tray.showWindow'),
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
click: (): void => {
|
click: (): void => {
|
||||||
showMainWindow()
|
showMainWindow()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'show-floating',
|
||||||
|
accelerator: showFloatingWindowShortcut,
|
||||||
|
label: floatingWindow?.isVisible() ? t('tray.hideFloatingWindow') : t('tray.showFloatingWindow'),
|
||||||
|
type: 'normal',
|
||||||
|
click: async (): Promise<void> => {
|
||||||
|
await triggerFloatingWindow()
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'rule',
|
id: 'rule',
|
||||||
label: '规则模式',
|
label: t('tray.ruleMode'),
|
||||||
accelerator: ruleModeShortcut,
|
accelerator: ruleModeShortcut,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: mode === 'rule',
|
checked: mode === 'rule',
|
||||||
@ -100,12 +117,13 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
await patchControledMihomoConfig({ mode: 'rule' })
|
await patchControledMihomoConfig({ mode: 'rule' })
|
||||||
await patchMihomoConfig({ mode: 'rule' })
|
await patchMihomoConfig({ mode: 'rule' })
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
|
mainWindow?.webContents.send('groupsUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'global',
|
id: 'global',
|
||||||
label: '全局模式',
|
label: t('tray.globalMode'),
|
||||||
accelerator: globalModeShortcut,
|
accelerator: globalModeShortcut,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: mode === 'global',
|
checked: mode === 'global',
|
||||||
@ -113,12 +131,13 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
await patchControledMihomoConfig({ mode: 'global' })
|
await patchControledMihomoConfig({ mode: 'global' })
|
||||||
await patchMihomoConfig({ mode: 'global' })
|
await patchMihomoConfig({ mode: 'global' })
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
|
mainWindow?.webContents.send('groupsUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'direct',
|
id: 'direct',
|
||||||
label: '直连模式',
|
label: t('tray.directMode'),
|
||||||
accelerator: directModeShortcut,
|
accelerator: directModeShortcut,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: mode === 'direct',
|
checked: mode === 'direct',
|
||||||
@ -126,13 +145,14 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
await patchControledMihomoConfig({ mode: 'direct' })
|
await patchControledMihomoConfig({ mode: 'direct' })
|
||||||
await patchMihomoConfig({ mode: 'direct' })
|
await patchMihomoConfig({ mode: 'direct' })
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
|
mainWindow?.webContents.send('groupsUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
label: '系统代理',
|
label: t('tray.systemProxy'),
|
||||||
accelerator: triggerSysProxyShortcut,
|
accelerator: triggerSysProxyShortcut,
|
||||||
checked: sysProxy.enable,
|
checked: sysProxy.enable,
|
||||||
click: async (item): Promise<void> => {
|
click: async (item): Promise<void> => {
|
||||||
@ -140,36 +160,43 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
try {
|
try {
|
||||||
await triggerSysProxy(enable)
|
await triggerSysProxy(enable)
|
||||||
await patchAppConfig({ sysProxy: { enable } })
|
await patchAppConfig({ sysProxy: { enable } })
|
||||||
|
mainWindow?.webContents.send('appConfigUpdated')
|
||||||
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
mainWindow?.webContents.send('appConfigUpdated')
|
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
label: '虚拟网卡',
|
label: t('tray.tun'),
|
||||||
accelerator: triggerTunShortcut,
|
accelerator: triggerTunShortcut,
|
||||||
checked: tun?.enable ?? false,
|
checked: tun?.enable ?? false,
|
||||||
click: async (item): Promise<void> => {
|
click: async (item): Promise<void> => {
|
||||||
const enable = item.checked
|
const enable = item.checked
|
||||||
|
try {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
|
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
|
||||||
} else {
|
} else {
|
||||||
await patchControledMihomoConfig({ tun: { enable } })
|
await patchControledMihomoConfig({ tun: { enable } })
|
||||||
}
|
}
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
|
floatingWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
await restartCore()
|
await restartCore()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
...groupsMenu,
|
...groupsMenu,
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
type: 'submenu',
|
type: 'submenu',
|
||||||
label: '订阅配置',
|
label: t('tray.profiles'),
|
||||||
submenu: items.map((item) => {
|
submenu: items.map((item) => {
|
||||||
return {
|
return {
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
@ -187,26 +214,26 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
type: 'submenu',
|
type: 'submenu',
|
||||||
label: '打开目录',
|
label: t('tray.openDirectories.title'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
label: '应用目录',
|
label: t('tray.openDirectories.appDir'),
|
||||||
click: (): Promise<string> => shell.openPath(dataDir())
|
click: (): Promise<string> => shell.openPath(dataDir())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
label: '工作目录',
|
label: t('tray.openDirectories.workDir'),
|
||||||
click: (): Promise<string> => shell.openPath(mihomoWorkDir())
|
click: (): Promise<string> => shell.openPath(mihomoWorkDir())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
label: '内核目录',
|
label: t('tray.openDirectories.coreDir'),
|
||||||
click: (): Promise<string> => shell.openPath(mihomoCoreDir())
|
click: (): Promise<string> => shell.openPath(mihomoCoreDir())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
label: '日志目录',
|
label: t('tray.openDirectories.logDir'),
|
||||||
click: (): Promise<string> => shell.openPath(logDir())
|
click: (): Promise<string> => shell.openPath(logDir())
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -214,7 +241,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
envType.length > 1
|
envType.length > 1
|
||||||
? {
|
? {
|
||||||
type: 'submenu',
|
type: 'submenu',
|
||||||
label: '复制环境变量',
|
label: t('tray.copyEnv'),
|
||||||
submenu: envType.map((type) => {
|
submenu: envType.map((type) => {
|
||||||
return {
|
return {
|
||||||
id: type,
|
id: type,
|
||||||
@ -228,7 +255,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
id: 'copyenv',
|
id: 'copyenv',
|
||||||
label: '复制环境变量',
|
label: t('tray.copyEnv'),
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
click: async (): Promise<void> => {
|
click: async (): Promise<void> => {
|
||||||
await copyEnv(envType[0])
|
await copyEnv(envType[0])
|
||||||
@ -237,14 +264,14 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
id: 'quitWithoutCore',
|
id: 'quitWithoutCore',
|
||||||
label: '轻量模式',
|
label: t('actions.lightMode.button'),
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
accelerator: quitWithoutCoreShortcut,
|
accelerator: quitWithoutCoreShortcut,
|
||||||
click: quitWithoutCore
|
click: quitWithoutCore
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'restart',
|
id: 'restart',
|
||||||
label: '重启应用',
|
label: t('actions.restartApp'),
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
accelerator: restartAppShortcut,
|
accelerator: restartAppShortcut,
|
||||||
click: (): void => {
|
click: (): void => {
|
||||||
@ -254,7 +281,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'quit',
|
id: 'quit',
|
||||||
label: '退出应用',
|
label: t('actions.quit.button'),
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
accelerator: 'CommandOrControl+Q',
|
accelerator: 'CommandOrControl+Q',
|
||||||
click: (): void => app.quit()
|
click: (): void => app.quit()
|
||||||
@ -282,7 +309,7 @@ export async function createTray(): Promise<void> {
|
|||||||
tray?.setIgnoreDoubleClickEvents(true)
|
tray?.setIgnoreDoubleClickEvents(true)
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
if (!useDockIcon) {
|
if (!useDockIcon) {
|
||||||
app.dock.hide()
|
hideDockIcon()
|
||||||
}
|
}
|
||||||
ipcMain.on('trayIconUpdate', async (_, png: string) => {
|
ipcMain.on('trayIconUpdate', async (_, png: string) => {
|
||||||
const image = nativeImage.createFromDataURL(png).resize({ height: 16 })
|
const image = nativeImage.createFromDataURL(png).resize({ height: 16 })
|
||||||
@ -290,11 +317,7 @@ export async function createTray(): Promise<void> {
|
|||||||
tray?.setImage(image)
|
tray?.setImage(image)
|
||||||
})
|
})
|
||||||
tray?.addListener('right-click', async () => {
|
tray?.addListener('right-click', async () => {
|
||||||
if (mainWindow?.isVisible()) {
|
triggerMainWindow()
|
||||||
mainWindow?.close()
|
|
||||||
} else {
|
|
||||||
showMainWindow()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
tray?.addListener('click', async () => {
|
tray?.addListener('click', async () => {
|
||||||
await updateTrayMenu()
|
await updateTrayMenu()
|
||||||
@ -302,11 +325,7 @@ export async function createTray(): Promise<void> {
|
|||||||
}
|
}
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
tray?.addListener('click', () => {
|
tray?.addListener('click', () => {
|
||||||
if (mainWindow?.isVisible()) {
|
triggerMainWindow()
|
||||||
mainWindow?.close()
|
|
||||||
} else {
|
|
||||||
showMainWindow()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
tray?.addListener('right-click', async () => {
|
tray?.addListener('right-click', async () => {
|
||||||
await updateTrayMenu()
|
await updateTrayMenu()
|
||||||
@ -314,11 +333,7 @@ export async function createTray(): Promise<void> {
|
|||||||
}
|
}
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
tray?.addListener('click', () => {
|
tray?.addListener('click', () => {
|
||||||
if (mainWindow?.isVisible()) {
|
triggerMainWindow()
|
||||||
mainWindow?.close()
|
|
||||||
} else {
|
|
||||||
showMainWindow()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
ipcMain.on('updateTrayMenu', async () => {
|
ipcMain.on('updateTrayMenu', async () => {
|
||||||
await updateTrayMenu()
|
await updateTrayMenu()
|
||||||
@ -359,3 +374,28 @@ export async function copyEnv(type: 'bash' | 'cmd' | 'powershell'): Promise<void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function showTrayIcon(): Promise<void> {
|
||||||
|
if (!tray) {
|
||||||
|
await createTray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeTrayIcon(): Promise<void> {
|
||||||
|
if (tray) {
|
||||||
|
tray.destroy()
|
||||||
|
}
|
||||||
|
tray = null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showDockIcon(): Promise<void> {
|
||||||
|
if (process.platform === 'darwin' && !app.dock.isVisible()) {
|
||||||
|
await app.dock.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hideDockIcon(): Promise<void> {
|
||||||
|
if (process.platform === 'darwin' && app.dock.isVisible()) {
|
||||||
|
app.dock.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { taskDir, exePath, homeDir } from '../utils/dirs'
|
import { exePath, homeDir, taskDir } from '../utils/dirs'
|
||||||
import { mkdir, readFile, rm, writeFile } from 'fs/promises'
|
import { mkdir, readFile, rm, writeFile } from 'fs/promises'
|
||||||
import { exec } from 'child_process'
|
import { exec } from 'child_process'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
@ -7,12 +7,9 @@ import path from 'path'
|
|||||||
|
|
||||||
const appName = 'mihomo-party'
|
const appName = 'mihomo-party'
|
||||||
|
|
||||||
const taskXml = `<?xml version="1.0" encoding="UTF-16"?>
|
function getTaskXml(): string {
|
||||||
|
return `<?xml version="1.0" encoding="UTF-16"?>
|
||||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||||
<RegistrationInfo>
|
|
||||||
<Date>${new Date().toISOString()}</Date>
|
|
||||||
<Author>${process.env.USERNAME}</Author>
|
|
||||||
</RegistrationInfo>
|
|
||||||
<Triggers>
|
<Triggers>
|
||||||
<LogonTrigger>
|
<LogonTrigger>
|
||||||
<Enabled>true</Enabled>
|
<Enabled>true</Enabled>
|
||||||
@ -26,12 +23,12 @@ const taskXml = `<?xml version="1.0" encoding="UTF-16"?>
|
|||||||
</Principal>
|
</Principal>
|
||||||
</Principals>
|
</Principals>
|
||||||
<Settings>
|
<Settings>
|
||||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
|
||||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||||
<AllowHardTerminate>false</AllowHardTerminate>
|
<AllowHardTerminate>false</AllowHardTerminate>
|
||||||
<StartWhenAvailable>true</StartWhenAvailable>
|
<StartWhenAvailable>false</StartWhenAvailable>
|
||||||
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
|
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||||
<IdleSettings>
|
<IdleSettings>
|
||||||
<StopOnIdleEnd>false</StopOnIdleEnd>
|
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||||
<RestartOnIdle>false</RestartOnIdle>
|
<RestartOnIdle>false</RestartOnIdle>
|
||||||
@ -42,21 +39,25 @@ const taskXml = `<?xml version="1.0" encoding="UTF-16"?>
|
|||||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||||
<WakeToRun>false</WakeToRun>
|
<WakeToRun>false</WakeToRun>
|
||||||
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
||||||
<Priority>7</Priority>
|
<Priority>3</Priority>
|
||||||
</Settings>
|
</Settings>
|
||||||
<Actions Context="Author">
|
<Actions Context="Author">
|
||||||
<Exec>
|
<Exec>
|
||||||
<Command>${exePath()}</Command>
|
<Command>"${path.join(taskDir(), `mihomo-party-run.exe`)}"</Command>
|
||||||
|
<Arguments>"${exePath()}"</Arguments>
|
||||||
</Exec>
|
</Exec>
|
||||||
</Actions>
|
</Actions>
|
||||||
</Task>
|
</Task>
|
||||||
`
|
`
|
||||||
|
}
|
||||||
|
|
||||||
export async function checkAutoRun(): Promise<boolean> {
|
export async function checkAutoRun(): Promise<boolean> {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execPromise(`schtasks /query /tn "${appName}"`)
|
const { stdout } = await execPromise(
|
||||||
|
`chcp 437 && %SystemRoot%\\System32\\schtasks.exe /query /tn "${appName}"`
|
||||||
|
)
|
||||||
return stdout.includes(appName)
|
return stdout.includes(appName)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false
|
return false
|
||||||
@ -81,8 +82,10 @@ export async function enableAutoRun(): Promise<void> {
|
|||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
const taskFilePath = path.join(taskDir(), `${appName}.xml`)
|
const taskFilePath = path.join(taskDir(), `${appName}.xml`)
|
||||||
await writeFile(taskFilePath, Buffer.from(`\ufeff${taskXml}`, 'utf-16le'))
|
await writeFile(taskFilePath, Buffer.from(`\ufeff${getTaskXml()}`, 'utf-16le'))
|
||||||
await execPromise(`schtasks /create /tn "${appName}" /xml "${taskFilePath}" /f`)
|
await execPromise(
|
||||||
|
`%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
@ -118,7 +121,7 @@ Categories=Utility;
|
|||||||
export async function disableAutoRun(): Promise<void> {
|
export async function disableAutoRun(): Promise<void> {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
await execPromise(`schtasks /delete /tn "${appName}" /f`)
|
await execPromise(`%SystemRoot%\\System32\\schtasks.exe /delete /tn "${appName}" /f`)
|
||||||
}
|
}
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { exec, execFile, execSync } from 'child_process'
|
import { exec, execFile, execSync, spawn } from 'child_process'
|
||||||
import { dialog, nativeTheme, shell } from 'electron'
|
import { app, dialog, nativeTheme, shell } from 'electron'
|
||||||
import { readFile } from 'fs/promises'
|
import { readFile } from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import {
|
import {
|
||||||
|
dataDir,
|
||||||
exePath,
|
exePath,
|
||||||
mihomoCorePath,
|
mihomoCorePath,
|
||||||
overridePath,
|
overridePath,
|
||||||
@ -44,9 +45,12 @@ export async function openUWPTool(): Promise<void> {
|
|||||||
export async function setupFirewall(): Promise<void> {
|
export async function setupFirewall(): Promise<void> {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
const removeCommand = `
|
const removeCommand = `
|
||||||
Remove-NetFirewallRule -DisplayName "mihomo" -ErrorAction SilentlyContinue
|
$rules = @("mihomo", "mihomo-alpha", "Mihomo Party")
|
||||||
Remove-NetFirewallRule -DisplayName "mihomo-alpha" -ErrorAction SilentlyContinue
|
foreach ($rule in $rules) {
|
||||||
Remove-NetFirewallRule -DisplayName "Mihomo Party" -ErrorAction SilentlyContinue
|
if (Get-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
const createCommand = `
|
const createCommand = `
|
||||||
New-NetFirewallRule -DisplayName "mihomo" -Direction Inbound -Action Allow -Program "${mihomoCorePath('mihomo')}" -Enabled True -Profile Any -ErrorAction SilentlyContinue
|
New-NetFirewallRule -DisplayName "mihomo" -Direction Inbound -Action Allow -Program "${mihomoCorePath('mihomo')}" -Enabled True -Profile Any -ErrorAction SilentlyContinue
|
||||||
@ -64,12 +68,9 @@ export function setNativeTheme(theme: 'system' | 'light' | 'dark'): void {
|
|||||||
nativeTheme.themeSource = theme
|
nativeTheme.themeSource = theme
|
||||||
}
|
}
|
||||||
|
|
||||||
const elevateTaskXml = `<?xml version="1.0" encoding="UTF-16"?>
|
function getElevateTaskXml(): string {
|
||||||
|
return `<?xml version="1.0" encoding="UTF-16"?>
|
||||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||||
<RegistrationInfo>
|
|
||||||
<Date>${new Date().toISOString()}</Date>
|
|
||||||
<Author>${process.env.USERNAME}</Author>
|
|
||||||
</RegistrationInfo>
|
|
||||||
<Triggers />
|
<Triggers />
|
||||||
<Principals>
|
<Principals>
|
||||||
<Principal id="Author">
|
<Principal id="Author">
|
||||||
@ -93,8 +94,8 @@ const elevateTaskXml = `<?xml version="1.0" encoding="UTF-16"?>
|
|||||||
<Hidden>false</Hidden>
|
<Hidden>false</Hidden>
|
||||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||||
<WakeToRun>false</WakeToRun>
|
<WakeToRun>false</WakeToRun>
|
||||||
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
|
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
||||||
<Priority>7</Priority>
|
<Priority>3</Priority>
|
||||||
</Settings>
|
</Settings>
|
||||||
<Actions Context="Author">
|
<Actions Context="Author">
|
||||||
<Exec>
|
<Exec>
|
||||||
@ -104,13 +105,46 @@ const elevateTaskXml = `<?xml version="1.0" encoding="UTF-16"?>
|
|||||||
</Actions>
|
</Actions>
|
||||||
</Task>
|
</Task>
|
||||||
`
|
`
|
||||||
|
}
|
||||||
|
|
||||||
export function createElevateTask(): void {
|
export function createElevateTask(): void {
|
||||||
const taskFilePath = path.join(taskDir(), `mihomo-party-run.xml`)
|
const taskFilePath = path.join(taskDir(), `mihomo-party-run.xml`)
|
||||||
writeFileSync(taskFilePath, Buffer.from(`\ufeff${elevateTaskXml}`, 'utf-16le'))
|
writeFileSync(taskFilePath, Buffer.from(`\ufeff${getElevateTaskXml()}`, 'utf-16le'))
|
||||||
execSync(`schtasks /create /tn "mihomo-party-run" /xml "${taskFilePath}" /f`)
|
|
||||||
copyFileSync(
|
copyFileSync(
|
||||||
path.join(resourcesFilesDir(), 'mihomo-party-run.exe'),
|
path.join(resourcesFilesDir(), 'mihomo-party-run.exe'),
|
||||||
path.join(taskDir(), 'mihomo-party-run.exe')
|
path.join(taskDir(), 'mihomo-party-run.exe')
|
||||||
)
|
)
|
||||||
|
execSync(
|
||||||
|
`%SystemRoot%\\System32\\schtasks.exe /create /tn "mihomo-party-run" /xml "${taskFilePath}" /f`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetAppConfig(): void {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
spawn(
|
||||||
|
'cmd',
|
||||||
|
[
|
||||||
|
'/C',
|
||||||
|
`"timeout /t 2 /nobreak >nul && rmdir /s /q "${dataDir()}" && start "" "${exePath()}""`
|
||||||
|
],
|
||||||
|
{
|
||||||
|
shell: true,
|
||||||
|
detached: true
|
||||||
|
}
|
||||||
|
).unref()
|
||||||
|
} else {
|
||||||
|
const script = `while kill -0 ${process.pid} 2>/dev/null; do
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
rm -rf '${dataDir()}'
|
||||||
|
${process.argv.join(' ')} & disown
|
||||||
|
exit
|
||||||
|
`
|
||||||
|
spawn('sh', ['-c', `"${script}"`], {
|
||||||
|
shell: true,
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
app.quit()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
import { triggerAutoProxy, triggerManualProxy } from '@mihomo-party/sysproxy'
|
import { triggerAutoProxy, triggerManualProxy } from '@mihomo-party/sysproxy'
|
||||||
import { getAppConfig, getControledMihomoConfig } from '../config'
|
import { getAppConfig, getControledMihomoConfig } from '../config'
|
||||||
import { pacPort } from '../resolve/server'
|
import { pacPort, startPacServer, stopPacServer } from '../resolve/server'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { resourcesFilesDir } from '../utils/dirs'
|
import { resourcesFilesDir } from '../utils/dirs'
|
||||||
import { net } from 'electron'
|
import { net } from 'electron'
|
||||||
|
import axios from 'axios'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
let defaultBypass: string[]
|
let defaultBypass: string[]
|
||||||
let triggerSysProxyTimer: NodeJS.Timeout | null = null
|
let triggerSysProxyTimer: NodeJS.Timeout | null = null
|
||||||
|
const helperSocketPath = '/tmp/mihomo-party-helper.sock'
|
||||||
|
|
||||||
if (process.platform === 'linux')
|
if (process.platform === 'linux')
|
||||||
defaultBypass = ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1']
|
defaultBypass = ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1']
|
||||||
@ -63,6 +66,7 @@ export async function triggerSysProxy(enable: boolean): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function enableSysProxy(): Promise<void> {
|
async function enableSysProxy(): Promise<void> {
|
||||||
|
await startPacServer()
|
||||||
const { sysProxy } = await getAppConfig()
|
const { sysProxy } = await getAppConfig()
|
||||||
const { mode, host, bypass = defaultBypass } = sysProxy
|
const { mode, host, bypass = defaultBypass } = sysProxy
|
||||||
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
|
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
|
||||||
@ -78,6 +82,16 @@ async function enableSysProxy(): Promise<void> {
|
|||||||
} catch {
|
} catch {
|
||||||
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
||||||
}
|
}
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
await helperRequest(() =>
|
||||||
|
axios.post(
|
||||||
|
'http://localhost/pac',
|
||||||
|
{ url: `http://${host || '127.0.0.1'}:${pacPort}/pac` },
|
||||||
|
{
|
||||||
|
socketPath: helperSocketPath
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
||||||
}
|
}
|
||||||
@ -96,6 +110,16 @@ async function enableSysProxy(): Promise<void> {
|
|||||||
} catch {
|
} catch {
|
||||||
triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(','))
|
triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(','))
|
||||||
}
|
}
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
await helperRequest(() =>
|
||||||
|
axios.post(
|
||||||
|
'http://localhost/global',
|
||||||
|
{ host: host || '127.0.0.1', port: port.toString(), bypass: bypass.join(',') },
|
||||||
|
{
|
||||||
|
socketPath: helperSocketPath
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(','))
|
triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(','))
|
||||||
}
|
}
|
||||||
@ -105,6 +129,7 @@ async function enableSysProxy(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function disableSysProxy(): Promise<void> {
|
async function disableSysProxy(): Promise<void> {
|
||||||
|
await stopPacServer()
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
try {
|
try {
|
||||||
@ -113,8 +138,85 @@ async function disableSysProxy(): Promise<void> {
|
|||||||
triggerAutoProxy(false, '')
|
triggerAutoProxy(false, '')
|
||||||
triggerManualProxy(false, '', 0, '')
|
triggerManualProxy(false, '', 0, '')
|
||||||
}
|
}
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
await helperRequest(() =>
|
||||||
|
axios.get('http://localhost/off', {
|
||||||
|
socketPath: helperSocketPath
|
||||||
|
})
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
triggerAutoProxy(false, '')
|
triggerAutoProxy(false, '')
|
||||||
triggerManualProxy(false, '', 0, '')
|
triggerManualProxy(false, '', 0, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to check if socket file exists
|
||||||
|
function isSocketFileExists(): boolean {
|
||||||
|
try {
|
||||||
|
return fs.existsSync(helperSocketPath)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to send signal to recreate socket
|
||||||
|
async function requestSocketRecreation(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Send SIGUSR1 signal to helper process to recreate socket
|
||||||
|
const { exec } = require('child_process')
|
||||||
|
const { promisify } = require('util')
|
||||||
|
const execPromise = promisify(exec)
|
||||||
|
|
||||||
|
// Use osascript with administrator privileges (same pattern as manualGrantCorePermition)
|
||||||
|
const shell = `pkill -USR1 -f party.mihomo.helper`
|
||||||
|
const command = `do shell script "${shell}" with administrator privileges`
|
||||||
|
await execPromise(`osascript -e '${command}'`)
|
||||||
|
|
||||||
|
// Wait a bit for socket recreation
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to send signal to helper:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper function for helper requests with auto-retry on socket issues
|
||||||
|
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1): Promise<unknown> {
|
||||||
|
let lastError: Error | null = null
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await requestFn()
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error
|
||||||
|
|
||||||
|
// Check if it's a connection error and socket file doesn't exist
|
||||||
|
if (attempt < maxRetries &&
|
||||||
|
((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' ||
|
||||||
|
(error as NodeJS.ErrnoException).code === 'ENOENT' ||
|
||||||
|
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
||||||
|
(error as Error).message?.includes('ENOENT'))) {
|
||||||
|
|
||||||
|
console.log(`Helper request failed (attempt ${attempt + 1}), checking socket file...`)
|
||||||
|
|
||||||
|
if (!isSocketFileExists()) {
|
||||||
|
console.log('Socket file missing, requesting recreation...')
|
||||||
|
try {
|
||||||
|
await requestSocketRecreation()
|
||||||
|
console.log('Socket recreation requested, retrying...')
|
||||||
|
continue
|
||||||
|
} catch (signalError) {
|
||||||
|
console.log('Failed to request socket recreation:', signalError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not a connection error or we've exhausted retries, throw the error
|
||||||
|
if (attempt === maxRetries) {
|
||||||
|
throw lastError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError
|
||||||
|
}
|
||||||
|
|||||||
@ -18,7 +18,13 @@ export function dataDir(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function taskDir(): string {
|
export function taskDir(): string {
|
||||||
const dir = path.join(app.getPath('userData'), 'tasks')
|
const userDataDir = app.getPath('userData')
|
||||||
|
// 确保 userData 目录存在
|
||||||
|
if (!existsSync(userDataDir)) {
|
||||||
|
mkdirSync(userDataDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir = path.join(userDataDir, 'tasks')
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
mkdirSync(dir, { recursive: true })
|
mkdirSync(dir, { recursive: true })
|
||||||
}
|
}
|
||||||
@ -102,12 +108,20 @@ export function mihomoWorkDir(): string {
|
|||||||
return path.join(dataDir(), 'work')
|
return path.join(dataDir(), 'work')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mihomoProfileWorkDir(id: string | undefined): string {
|
||||||
|
return path.join(mihomoWorkDir(), id || 'default')
|
||||||
|
}
|
||||||
|
|
||||||
export function mihomoTestDir(): string {
|
export function mihomoTestDir(): string {
|
||||||
return path.join(dataDir(), 'test')
|
return path.join(dataDir(), 'test')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mihomoWorkConfigPath(): string {
|
export function mihomoWorkConfigPath(id: string | undefined): string {
|
||||||
|
if (id === 'work') {
|
||||||
return path.join(mihomoWorkDir(), 'config.yaml')
|
return path.join(mihomoWorkDir(), 'config.yaml')
|
||||||
|
} else {
|
||||||
|
return path.join(mihomoProfileWorkDir(id), 'config.yaml')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logDir(): string {
|
export function logDir(): string {
|
||||||
@ -119,3 +133,9 @@ export function logPath(): string {
|
|||||||
const name = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
|
const name = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
|
||||||
return path.join(logDir(), `${name}.log`)
|
return path.join(logDir(), `${name}.log`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function substoreLogPath(): string {
|
||||||
|
const date = new Date()
|
||||||
|
const name = `sub-store-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
|
||||||
|
return path.join(logDir(), `${name}.log`)
|
||||||
|
}
|
||||||
|
|||||||
17
src/main/utils/image.ts
Normal file
17
src/main/utils/image.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { getControledMihomoConfig } from '../config'
|
||||||
|
|
||||||
|
export async function getImageDataURL(url: string): Promise<string> {
|
||||||
|
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
|
||||||
|
const res = await axios.get(url, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
proxy: {
|
||||||
|
protocol: 'http',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const mimeType = res.headers['content-type']
|
||||||
|
const dataURL = `data:${mimeType};base64,${Buffer.from(res.data).toString('base64')}`
|
||||||
|
return dataURL
|
||||||
|
}
|
||||||
@ -22,10 +22,16 @@ import {
|
|||||||
defaultProfileConfig
|
defaultProfileConfig
|
||||||
} from './template'
|
} from './template'
|
||||||
import yaml from 'yaml'
|
import yaml from 'yaml'
|
||||||
import { mkdir, writeFile, copyFile, rm, readdir } from 'fs/promises'
|
import { mkdir, writeFile, rm, readdir, cp, stat } from 'fs/promises'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { startPacServer, startSubStoreServer } from '../resolve/server'
|
import {
|
||||||
|
startPacServer,
|
||||||
|
startSubStoreBackendServer,
|
||||||
|
startSubStoreFrontendServer
|
||||||
|
} from '../resolve/server'
|
||||||
import { triggerSysProxy } from '../sys/sysproxy'
|
import { triggerSysProxy } from '../sys/sysproxy'
|
||||||
import {
|
import {
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
@ -36,7 +42,32 @@ import {
|
|||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import { startSSIDCheck } from '../sys/ssid'
|
import { startSSIDCheck } from '../sys/ssid'
|
||||||
|
|
||||||
|
async function fixDataDirPermissions(): Promise<void> {
|
||||||
|
if (process.platform !== 'darwin') return
|
||||||
|
|
||||||
|
const dataDirPath = dataDir()
|
||||||
|
if (!existsSync(dataDirPath)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await stat(dataDirPath)
|
||||||
|
const currentUid = process.getuid?.() || 0
|
||||||
|
|
||||||
|
if (stats.uid === 0 && currentUid !== 0) {
|
||||||
|
const execPromise = promisify(exec)
|
||||||
|
const username = process.env.USER || process.env.LOGNAME
|
||||||
|
if (username) {
|
||||||
|
await execPromise(`chown -R "${username}:staff" "${dataDirPath}"`)
|
||||||
|
await execPromise(`chmod -R u+rwX "${dataDirPath}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function initDirs(): Promise<void> {
|
async function initDirs(): Promise<void> {
|
||||||
|
await fixDataDirPermissions()
|
||||||
|
|
||||||
if (!existsSync(dataDir())) {
|
if (!existsSync(dataDir())) {
|
||||||
await mkdir(dataDir())
|
await mkdir(dataDir())
|
||||||
}
|
}
|
||||||
@ -84,20 +115,23 @@ async function initConfig(): Promise<void> {
|
|||||||
async function initFiles(): Promise<void> {
|
async function initFiles(): Promise<void> {
|
||||||
const copy = async (file: string): Promise<void> => {
|
const copy = async (file: string): Promise<void> => {
|
||||||
const targetPath = path.join(mihomoWorkDir(), file)
|
const targetPath = path.join(mihomoWorkDir(), file)
|
||||||
const testTargrtPath = path.join(mihomoTestDir(), file)
|
const testTargetPath = path.join(mihomoTestDir(), file)
|
||||||
const sourcePath = path.join(resourcesFilesDir(), file)
|
const sourcePath = path.join(resourcesFilesDir(), file)
|
||||||
if (!existsSync(targetPath) && existsSync(sourcePath)) {
|
if (!existsSync(targetPath) && existsSync(sourcePath)) {
|
||||||
await copyFile(sourcePath, targetPath)
|
await cp(sourcePath, targetPath, { recursive: true })
|
||||||
}
|
}
|
||||||
if (!existsSync(testTargrtPath) && existsSync(sourcePath)) {
|
if (!existsSync(testTargetPath) && existsSync(sourcePath)) {
|
||||||
await copyFile(sourcePath, testTargrtPath)
|
await cp(sourcePath, testTargetPath, { recursive: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
copy('country.mmdb'),
|
copy('country.mmdb'),
|
||||||
|
copy('geoip.metadb'),
|
||||||
copy('geoip.dat'),
|
copy('geoip.dat'),
|
||||||
copy('geosite.dat'),
|
copy('geosite.dat'),
|
||||||
copy('ASN.mmdb')
|
copy('ASN.mmdb'),
|
||||||
|
copy('sub-store.bundle.js'),
|
||||||
|
copy('sub-store-frontend')
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +139,7 @@ async function cleanup(): Promise<void> {
|
|||||||
// update cache
|
// update cache
|
||||||
const files = await readdir(dataDir())
|
const files = await readdir(dataDir())
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.endsWith('.exe') || file.endsWith('.dmg')) {
|
if (file.endsWith('.exe') || file.endsWith('.pkg') || file.endsWith('.7z')) {
|
||||||
try {
|
try {
|
||||||
await rm(path.join(dataDir(), file))
|
await rm(path.join(dataDir(), file))
|
||||||
} catch {
|
} catch {
|
||||||
@ -148,9 +182,15 @@ async function migration(): Promise<void> {
|
|||||||
],
|
],
|
||||||
appTheme = 'system',
|
appTheme = 'system',
|
||||||
envType = [process.platform === 'win32' ? 'powershell' : 'bash'],
|
envType = [process.platform === 'win32' ? 'powershell' : 'bash'],
|
||||||
useSubStore = true
|
useSubStore = true,
|
||||||
|
showFloatingWindow = false,
|
||||||
|
disableTray = false,
|
||||||
|
encryptedPassword
|
||||||
} = await getAppConfig()
|
} = await getAppConfig()
|
||||||
const {
|
const {
|
||||||
|
'external-controller-pipe': externalControllerPipe,
|
||||||
|
'external-controller-unix': externalControllerUnix,
|
||||||
|
'external-controller': externalController,
|
||||||
'skip-auth-prefixes': skipAuthPrefixes,
|
'skip-auth-prefixes': skipAuthPrefixes,
|
||||||
authentication,
|
authentication,
|
||||||
'bind-address': bindAddress,
|
'bind-address': bindAddress,
|
||||||
@ -189,6 +229,26 @@ async function migration(): Promise<void> {
|
|||||||
if (typeof envType === 'string') {
|
if (typeof envType === 'string') {
|
||||||
await patchAppConfig({ envType: [envType] })
|
await patchAppConfig({ envType: [envType] })
|
||||||
}
|
}
|
||||||
|
// use unix socket
|
||||||
|
if (externalControllerUnix) {
|
||||||
|
await patchControledMihomoConfig({ 'external-controller-unix': undefined })
|
||||||
|
}
|
||||||
|
// use named pipe
|
||||||
|
if (externalControllerPipe) {
|
||||||
|
await patchControledMihomoConfig({
|
||||||
|
'external-controller-pipe': undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (externalController === undefined) {
|
||||||
|
await patchControledMihomoConfig({ 'external-controller': '' })
|
||||||
|
}
|
||||||
|
if (!showFloatingWindow && disableTray) {
|
||||||
|
await patchAppConfig({ disableTray: false })
|
||||||
|
}
|
||||||
|
// remove password
|
||||||
|
if (encryptedPassword) {
|
||||||
|
await patchAppConfig({ encryptedPassword: undefined })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initDeeplink(): void {
|
function initDeeplink(): void {
|
||||||
@ -209,10 +269,17 @@ export async function init(): Promise<void> {
|
|||||||
await migration()
|
await migration()
|
||||||
await initFiles()
|
await initFiles()
|
||||||
await cleanup()
|
await cleanup()
|
||||||
await startPacServer()
|
await startSubStoreFrontendServer()
|
||||||
await startSubStoreServer()
|
await startSubStoreBackendServer()
|
||||||
const { sysProxy } = await getAppConfig()
|
const { sysProxy } = await getAppConfig()
|
||||||
|
try {
|
||||||
|
if (sysProxy.enable) {
|
||||||
|
await startPacServer()
|
||||||
|
}
|
||||||
await triggerSysProxy(sysProxy.enable)
|
await triggerSysProxy(sysProxy.enable)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
await startSSIDCheck()
|
await startSSIDCheck()
|
||||||
|
|
||||||
initDeeplink()
|
initDeeplink()
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { app, dialog, ipcMain, safeStorage } from 'electron'
|
import { app, dialog, ipcMain } from 'electron'
|
||||||
import {
|
import {
|
||||||
mihomoChangeProxy,
|
mihomoChangeProxy,
|
||||||
mihomoCloseAllConnections,
|
mihomoCloseAllConnections,
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
mihomoProxyProviders,
|
mihomoProxyProviders,
|
||||||
mihomoRuleProviders,
|
mihomoRuleProviders,
|
||||||
mihomoRules,
|
mihomoRules,
|
||||||
|
mihomoUnfixedProxy,
|
||||||
mihomoUpdateProxyProviders,
|
mihomoUpdateProxyProviders,
|
||||||
mihomoUpdateRuleProviders,
|
mihomoUpdateRuleProviders,
|
||||||
mihomoUpgrade,
|
mihomoUpgrade,
|
||||||
@ -30,6 +31,8 @@ import {
|
|||||||
removeProfileItem,
|
removeProfileItem,
|
||||||
changeCurrentProfile,
|
changeCurrentProfile,
|
||||||
getProfileStr,
|
getProfileStr,
|
||||||
|
getFileStr,
|
||||||
|
setFileStr,
|
||||||
setProfileStr,
|
setProfileStr,
|
||||||
updateProfileItem,
|
updateProfileItem,
|
||||||
setProfileConfig,
|
setProfileConfig,
|
||||||
@ -42,13 +45,16 @@ import {
|
|||||||
setOverride,
|
setOverride,
|
||||||
updateOverrideItem
|
updateOverrideItem
|
||||||
} from '../config'
|
} from '../config'
|
||||||
import { startSubStoreServer, subStoreFrontendPort, subStorePort } from '../resolve/server'
|
|
||||||
import {
|
import {
|
||||||
isEncryptionAvailable,
|
startSubStoreFrontendServer,
|
||||||
manualGrantCorePermition,
|
startSubStoreBackendServer,
|
||||||
quitWithoutCore,
|
stopSubStoreFrontendServer,
|
||||||
restartCore
|
stopSubStoreBackendServer,
|
||||||
} from '../core/manager'
|
downloadSubStore,
|
||||||
|
subStoreFrontendPort,
|
||||||
|
subStorePort
|
||||||
|
} from '../resolve/server'
|
||||||
|
import { manualGrantCorePermition, quitWithoutCore, restartCore } from '../core/manager'
|
||||||
import { triggerSysProxy } from '../sys/sysproxy'
|
import { triggerSysProxy } from '../sys/sysproxy'
|
||||||
import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater'
|
import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater'
|
||||||
import {
|
import {
|
||||||
@ -56,15 +62,16 @@ import {
|
|||||||
openFile,
|
openFile,
|
||||||
openUWPTool,
|
openUWPTool,
|
||||||
readTextFile,
|
readTextFile,
|
||||||
|
resetAppConfig,
|
||||||
setNativeTheme,
|
setNativeTheme,
|
||||||
setupFirewall
|
setupFirewall
|
||||||
} from '../sys/misc'
|
} from '../sys/misc'
|
||||||
import { getRuntimeConfig, getRuntimeConfigStr } from '../core/factory'
|
import { getRuntimeConfig, getRuntimeConfigStr } from '../core/factory'
|
||||||
import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore } from '../resolve/backup'
|
import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore } from '../resolve/backup'
|
||||||
import { getInterfaces } from '../sys/interface'
|
import { getInterfaces } from '../sys/interface'
|
||||||
import { copyEnv } from '../resolve/tray'
|
import { closeTrayIcon, copyEnv, showTrayIcon } from '../resolve/tray'
|
||||||
import { registerShortcut } from '../resolve/shortcut'
|
import { registerShortcut } from '../resolve/shortcut'
|
||||||
import { mainWindow } from '..'
|
import { closeMainWindow, mainWindow, showMainWindow, triggerMainWindow } from '..'
|
||||||
import {
|
import {
|
||||||
applyTheme,
|
applyTheme,
|
||||||
fetchThemes,
|
fetchThemes,
|
||||||
@ -78,6 +85,11 @@ import { logDir } from './dirs'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import v8 from 'v8'
|
import v8 from 'v8'
|
||||||
import { getGistUrl } from '../resolve/gistApi'
|
import { getGistUrl } from '../resolve/gistApi'
|
||||||
|
import { getImageDataURL } from './image'
|
||||||
|
import { startMonitor } from '../resolve/trafficMonitor'
|
||||||
|
import { closeFloatingWindow, showContextMenu, showFloatingWindow } from '../resolve/floatingWindow'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
import { addProfileUpdater } from '../core/profileUpdater'
|
||||||
|
|
||||||
function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
fn: (...args: any[]) => Promise<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
fn: (...args: any[]) => Promise<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -119,6 +131,7 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('mihomoChangeProxy', (_e, group, proxy) =>
|
ipcMain.handle('mihomoChangeProxy', (_e, group, proxy) =>
|
||||||
ipcErrorWrapper(mihomoChangeProxy)(group, proxy)
|
ipcErrorWrapper(mihomoChangeProxy)(group, proxy)
|
||||||
)
|
)
|
||||||
|
ipcMain.handle('mihomoUnfixedProxy', (_e, group) => ipcErrorWrapper(mihomoUnfixedProxy)(group))
|
||||||
ipcMain.handle('mihomoUpgradeGeo', ipcErrorWrapper(mihomoUpgradeGeo))
|
ipcMain.handle('mihomoUpgradeGeo', ipcErrorWrapper(mihomoUpgradeGeo))
|
||||||
ipcMain.handle('mihomoUpgrade', ipcErrorWrapper(mihomoUpgrade))
|
ipcMain.handle('mihomoUpgrade', ipcErrorWrapper(mihomoUpgrade))
|
||||||
ipcMain.handle('mihomoProxyDelay', (_e, proxy, url) =>
|
ipcMain.handle('mihomoProxyDelay', (_e, proxy, url) =>
|
||||||
@ -144,11 +157,14 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('getCurrentProfileItem', ipcErrorWrapper(getCurrentProfileItem))
|
ipcMain.handle('getCurrentProfileItem', ipcErrorWrapper(getCurrentProfileItem))
|
||||||
ipcMain.handle('getProfileItem', (_e, id) => ipcErrorWrapper(getProfileItem)(id))
|
ipcMain.handle('getProfileItem', (_e, id) => ipcErrorWrapper(getProfileItem)(id))
|
||||||
ipcMain.handle('getProfileStr', (_e, id) => ipcErrorWrapper(getProfileStr)(id))
|
ipcMain.handle('getProfileStr', (_e, id) => ipcErrorWrapper(getProfileStr)(id))
|
||||||
|
ipcMain.handle('getFileStr', (_e, path) => ipcErrorWrapper(getFileStr)(path))
|
||||||
|
ipcMain.handle('setFileStr', (_e, path, str) => ipcErrorWrapper(setFileStr)(path, str))
|
||||||
ipcMain.handle('setProfileStr', (_e, id, str) => ipcErrorWrapper(setProfileStr)(id, str))
|
ipcMain.handle('setProfileStr', (_e, id, str) => ipcErrorWrapper(setProfileStr)(id, str))
|
||||||
ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item))
|
ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item))
|
||||||
ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))
|
ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))
|
||||||
ipcMain.handle('addProfileItem', (_e, item) => ipcErrorWrapper(addProfileItem)(item))
|
ipcMain.handle('addProfileItem', (_e, item) => ipcErrorWrapper(addProfileItem)(item))
|
||||||
ipcMain.handle('removeProfileItem', (_e, id) => ipcErrorWrapper(removeProfileItem)(id))
|
ipcMain.handle('removeProfileItem', (_e, id) => ipcErrorWrapper(removeProfileItem)(id))
|
||||||
|
ipcMain.handle('addProfileUpdater', (_e, item) => ipcErrorWrapper(addProfileUpdater)(item))
|
||||||
ipcMain.handle('getOverrideConfig', (_e, force) => ipcErrorWrapper(getOverrideConfig)(force))
|
ipcMain.handle('getOverrideConfig', (_e, force) => ipcErrorWrapper(getOverrideConfig)(force))
|
||||||
ipcMain.handle('setOverrideConfig', (_e, config) => ipcErrorWrapper(setOverrideConfig)(config))
|
ipcMain.handle('setOverrideConfig', (_e, config) => ipcErrorWrapper(setOverrideConfig)(config))
|
||||||
ipcMain.handle('getOverrideItem', (_e, id) => ipcErrorWrapper(getOverrideItem)(id))
|
ipcMain.handle('getOverrideItem', (_e, id) => ipcErrorWrapper(getOverrideItem)(id))
|
||||||
@ -158,12 +174,9 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('getOverride', (_e, id, ext) => ipcErrorWrapper(getOverride)(id, ext))
|
ipcMain.handle('getOverride', (_e, id, ext) => ipcErrorWrapper(getOverride)(id, ext))
|
||||||
ipcMain.handle('setOverride', (_e, id, ext, str) => ipcErrorWrapper(setOverride)(id, ext, str))
|
ipcMain.handle('setOverride', (_e, id, ext, str) => ipcErrorWrapper(setOverride)(id, ext, str))
|
||||||
ipcMain.handle('restartCore', ipcErrorWrapper(restartCore))
|
ipcMain.handle('restartCore', ipcErrorWrapper(restartCore))
|
||||||
|
ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached))
|
||||||
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
|
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
|
||||||
ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable)
|
ipcMain.handle('manualGrantCorePermition', () => ipcErrorWrapper(manualGrantCorePermition)())
|
||||||
ipcMain.handle('encryptString', (_e, str) => encryptString(str))
|
|
||||||
ipcMain.handle('manualGrantCorePermition', (_e, password) =>
|
|
||||||
ipcErrorWrapper(manualGrantCorePermition)(password)
|
|
||||||
)
|
|
||||||
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
|
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
|
||||||
ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
|
ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
|
||||||
ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))
|
ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))
|
||||||
@ -184,7 +197,13 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('registerShortcut', (_e, oldShortcut, newShortcut, action) =>
|
ipcMain.handle('registerShortcut', (_e, oldShortcut, newShortcut, action) =>
|
||||||
ipcErrorWrapper(registerShortcut)(oldShortcut, newShortcut, action)
|
ipcErrorWrapper(registerShortcut)(oldShortcut, newShortcut, action)
|
||||||
)
|
)
|
||||||
ipcMain.handle('startSubStoreServer', () => ipcErrorWrapper(startSubStoreServer)())
|
ipcMain.handle('startSubStoreFrontendServer', () =>
|
||||||
|
ipcErrorWrapper(startSubStoreFrontendServer)()
|
||||||
|
)
|
||||||
|
ipcMain.handle('stopSubStoreFrontendServer', () => ipcErrorWrapper(stopSubStoreFrontendServer)())
|
||||||
|
ipcMain.handle('startSubStoreBackendServer', () => ipcErrorWrapper(startSubStoreBackendServer)())
|
||||||
|
ipcMain.handle('stopSubStoreBackendServer', () => ipcErrorWrapper(stopSubStoreBackendServer)())
|
||||||
|
ipcMain.handle('downloadSubStore', () => ipcErrorWrapper(downloadSubStore)())
|
||||||
ipcMain.handle('subStorePort', () => subStorePort)
|
ipcMain.handle('subStorePort', () => subStorePort)
|
||||||
ipcMain.handle('subStoreFrontendPort', () => subStoreFrontendPort)
|
ipcMain.handle('subStoreFrontendPort', () => subStoreFrontendPort)
|
||||||
ipcMain.handle('subStoreSubs', () => ipcErrorWrapper(subStoreSubs)())
|
ipcMain.handle('subStoreSubs', () => ipcErrorWrapper(subStoreSubs)())
|
||||||
@ -195,7 +214,9 @@ export function registerIpcMainHandlers(): void {
|
|||||||
})
|
})
|
||||||
ipcMain.handle('setTitleBarOverlay', (_e, overlay) =>
|
ipcMain.handle('setTitleBarOverlay', (_e, overlay) =>
|
||||||
ipcErrorWrapper(async (overlay): Promise<void> => {
|
ipcErrorWrapper(async (overlay): Promise<void> => {
|
||||||
mainWindow?.setTitleBarOverlay(overlay)
|
if (mainWindow && typeof mainWindow.setTitleBarOverlay === 'function') {
|
||||||
|
mainWindow.setTitleBarOverlay(overlay)
|
||||||
|
}
|
||||||
})(overlay)
|
})(overlay)
|
||||||
)
|
)
|
||||||
ipcMain.handle('setAlwaysOnTop', (_e, alwaysOnTop) => {
|
ipcMain.handle('setAlwaysOnTop', (_e, alwaysOnTop) => {
|
||||||
@ -204,6 +225,14 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('isAlwaysOnTop', () => {
|
ipcMain.handle('isAlwaysOnTop', () => {
|
||||||
return mainWindow?.isAlwaysOnTop()
|
return mainWindow?.isAlwaysOnTop()
|
||||||
})
|
})
|
||||||
|
ipcMain.handle('showTrayIcon', () => ipcErrorWrapper(showTrayIcon)())
|
||||||
|
ipcMain.handle('closeTrayIcon', () => ipcErrorWrapper(closeTrayIcon)())
|
||||||
|
ipcMain.handle('showMainWindow', showMainWindow)
|
||||||
|
ipcMain.handle('closeMainWindow', closeMainWindow)
|
||||||
|
ipcMain.handle('triggerMainWindow', triggerMainWindow)
|
||||||
|
ipcMain.handle('showFloatingWindow', () => ipcErrorWrapper(showFloatingWindow)())
|
||||||
|
ipcMain.handle('closeFloatingWindow', () => ipcErrorWrapper(closeFloatingWindow)())
|
||||||
|
ipcMain.handle('showContextMenu', () => ipcErrorWrapper(showContextMenu)())
|
||||||
ipcMain.handle('openFile', (_e, type, id, ext) => openFile(type, id, ext))
|
ipcMain.handle('openFile', (_e, type, id, ext) => openFile(type, id, ext))
|
||||||
ipcMain.handle('openDevTools', () => {
|
ipcMain.handle('openDevTools', () => {
|
||||||
mainWindow?.webContents.openDevTools()
|
mainWindow?.webContents.openDevTools()
|
||||||
@ -211,6 +240,7 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('createHeapSnapshot', () => {
|
ipcMain.handle('createHeapSnapshot', () => {
|
||||||
v8.writeHeapSnapshot(path.join(logDir(), `${Date.now()}.heapsnapshot`))
|
v8.writeHeapSnapshot(path.join(logDir(), `${Date.now()}.heapsnapshot`))
|
||||||
})
|
})
|
||||||
|
ipcMain.handle('getImageDataURL', (_e, url) => ipcErrorWrapper(getImageDataURL)(url))
|
||||||
ipcMain.handle('resolveThemes', () => ipcErrorWrapper(resolveThemes)())
|
ipcMain.handle('resolveThemes', () => ipcErrorWrapper(resolveThemes)())
|
||||||
ipcMain.handle('fetchThemes', () => ipcErrorWrapper(fetchThemes)())
|
ipcMain.handle('fetchThemes', () => ipcErrorWrapper(fetchThemes)())
|
||||||
ipcMain.handle('importThemes', (_e, file) => ipcErrorWrapper(importThemes)(file))
|
ipcMain.handle('importThemes', (_e, file) => ipcErrorWrapper(importThemes)(file))
|
||||||
@ -221,14 +251,18 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('alert', (_e, msg) => {
|
ipcMain.handle('alert', (_e, msg) => {
|
||||||
dialog.showErrorBox('Mihomo Party', msg)
|
dialog.showErrorBox('Mihomo Party', msg)
|
||||||
})
|
})
|
||||||
|
ipcMain.handle('resetAppConfig', resetAppConfig)
|
||||||
ipcMain.handle('relaunchApp', () => {
|
ipcMain.handle('relaunchApp', () => {
|
||||||
app.relaunch()
|
app.relaunch()
|
||||||
app.quit()
|
app.quit()
|
||||||
})
|
})
|
||||||
ipcMain.handle('quitWithoutCore', ipcErrorWrapper(quitWithoutCore))
|
ipcMain.handle('quitWithoutCore', ipcErrorWrapper(quitWithoutCore))
|
||||||
ipcMain.handle('quitApp', () => app.quit())
|
ipcMain.handle('quitApp', () => app.quit())
|
||||||
}
|
|
||||||
|
|
||||||
function encryptString(str: string): number[] {
|
// Add language change handler
|
||||||
return safeStorage.encryptString(str).toJSON().data
|
ipcMain.handle('changeLanguage', async (_e, lng) => {
|
||||||
|
await i18next.changeLanguage(lng)
|
||||||
|
// 触发托盘菜单更新
|
||||||
|
ipcMain.emit('updateTrayMenu')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,27 +3,36 @@ function isObject(item: any): boolean {
|
|||||||
return item && typeof item === 'object' && !Array.isArray(item)
|
return item && typeof item === 'object' && !Array.isArray(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimWrap(str: string): string {
|
||||||
|
if (str.startsWith('<') && str.endsWith('>')) {
|
||||||
|
return str.slice(1, -1)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
export function deepMerge<T extends object>(target: T, other: Partial<T>): T {
|
export function deepMerge<T extends object>(target: T, other: Partial<T>): T {
|
||||||
for (const key in other) {
|
for (const key in other) {
|
||||||
if (isObject(other[key])) {
|
if (isObject(other[key])) {
|
||||||
if (key.endsWith('!')) {
|
if (key.endsWith('!')) {
|
||||||
const k = key.slice(0, -1)
|
const k = trimWrap(key.slice(0, -1))
|
||||||
target[k] = other[key]
|
target[k] = other[key]
|
||||||
} else {
|
} else {
|
||||||
if (!target[key]) Object.assign(target, { [key]: {} })
|
const k = trimWrap(key)
|
||||||
deepMerge(target[key] as object, other[key] as object)
|
if (!target[k]) Object.assign(target, { [k]: {} })
|
||||||
|
deepMerge(target[k] as object, other[k] as object)
|
||||||
}
|
}
|
||||||
} else if (Array.isArray(other[key])) {
|
} else if (Array.isArray(other[key])) {
|
||||||
if (key.startsWith('+')) {
|
if (key.startsWith('+')) {
|
||||||
const k = key.slice(1)
|
const k = trimWrap(key.slice(1))
|
||||||
if (!target[k]) Object.assign(target, { [k]: [] })
|
if (!target[k]) Object.assign(target, { [k]: [] })
|
||||||
target[k] = [...other[key], ...(target[k] as never[])]
|
target[k] = [...other[key], ...(target[k] as never[])]
|
||||||
} else if (key.endsWith('+')) {
|
} else if (key.endsWith('+')) {
|
||||||
const k = key.slice(0, -1)
|
const k = trimWrap(key.slice(0, -1))
|
||||||
if (!target[k]) Object.assign(target, { [k]: [] })
|
if (!target[k]) Object.assign(target, { [k]: [] })
|
||||||
target[k] = [...(target[k] as never[]), ...other[key]]
|
target[k] = [...(target[k] as never[]), ...other[key]]
|
||||||
} else {
|
} else {
|
||||||
Object.assign(target, { [key]: other[key] })
|
const k = trimWrap(key)
|
||||||
|
Object.assign(target, { [k]: other[key] })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Object.assign(target, { [key]: other[key] })
|
Object.assign(target, { [key]: other[key] })
|
||||||
|
|||||||
@ -32,12 +32,12 @@ export const defaultConfig: IAppConfig = {
|
|||||||
'log',
|
'log',
|
||||||
'substore'
|
'substore'
|
||||||
],
|
],
|
||||||
|
siderWidth: 250,
|
||||||
sysProxy: { enable: false, mode: 'manual' }
|
sysProxy: { enable: false, mode: 'manual' }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
|
export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
|
||||||
'external-controller': '127.0.0.1:9090',
|
'external-controller': '',
|
||||||
secret: '',
|
|
||||||
ipv6: true,
|
ipv6: true,
|
||||||
mode: 'rule',
|
mode: 'rule',
|
||||||
'mixed-port': 7890,
|
'mixed-port': 7890,
|
||||||
@ -46,7 +46,7 @@ export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
|
|||||||
'redir-port': 0,
|
'redir-port': 0,
|
||||||
'tproxy-port': 0,
|
'tproxy-port': 0,
|
||||||
'allow-lan': false,
|
'allow-lan': false,
|
||||||
'unified-delay': false,
|
'unified-delay': true,
|
||||||
'tcp-concurrent': false,
|
'tcp-concurrent': false,
|
||||||
'log-level': 'info',
|
'log-level': 'info',
|
||||||
'find-process-mode': 'strict',
|
'find-process-mode': 'strict',
|
||||||
@ -63,6 +63,7 @@ export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
|
|||||||
'auto-redirect': false,
|
'auto-redirect': false,
|
||||||
'auto-detect-interface': true,
|
'auto-detect-interface': true,
|
||||||
'dns-hijack': ['any:53'],
|
'dns-hijack': ['any:53'],
|
||||||
|
'route-exclude-address': [],
|
||||||
mtu: 1500
|
mtu: 1500
|
||||||
},
|
},
|
||||||
dns: {
|
dns: {
|
||||||
@ -73,8 +74,9 @@ export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
|
|||||||
'fake-ip-filter': ['*', '+.lan', '+.local', 'time.*.com', 'ntp.*.com', '+.market.xiaomi.com'],
|
'fake-ip-filter': ['*', '+.lan', '+.local', 'time.*.com', 'ntp.*.com', '+.market.xiaomi.com'],
|
||||||
'use-hosts': false,
|
'use-hosts': false,
|
||||||
'use-system-hosts': false,
|
'use-system-hosts': false,
|
||||||
nameserver: ['https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query'],
|
nameserver: ['https://120.53.53.53/dns-query', 'https://223.5.5.5/dns-query'],
|
||||||
'proxy-server-nameserver': ['https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query']
|
'proxy-server-nameserver': ['https://120.53.53.53/dns-query', 'https://223.5.5.5/dns-query'],
|
||||||
|
'direct-nameserver': []
|
||||||
},
|
},
|
||||||
sniffer: {
|
sniffer: {
|
||||||
enable: true,
|
enable: true,
|
||||||
@ -88,12 +90,23 @@ export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
|
|||||||
},
|
},
|
||||||
TLS: {
|
TLS: {
|
||||||
ports: [443]
|
ports: [443]
|
||||||
},
|
|
||||||
QUIC: {
|
|
||||||
ports: [443]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'skip-domain': ['+.push.apple.com']
|
'skip-domain': ['+.push.apple.com'],
|
||||||
|
'skip-dst-address': [
|
||||||
|
'91.105.192.0/23',
|
||||||
|
'91.108.4.0/22',
|
||||||
|
'91.108.8.0/21',
|
||||||
|
'91.108.16.0/21',
|
||||||
|
'91.108.56.0/22',
|
||||||
|
'95.161.64.0/20',
|
||||||
|
'149.154.160.0/20',
|
||||||
|
'185.76.151.0/24',
|
||||||
|
'2001:67c:4e8::/48',
|
||||||
|
'2001:b28:f23c::/47',
|
||||||
|
'2001:b28:f23f::/48',
|
||||||
|
'2a0a:f280:203::/48'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
profile: {
|
profile: {
|
||||||
'store-selected': true,
|
'store-selected': true,
|
||||||
@ -105,7 +118,7 @@ export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
|
|||||||
'geox-url': {
|
'geox-url': {
|
||||||
geoip: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat',
|
geoip: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat',
|
||||||
geosite: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat',
|
geosite: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat',
|
||||||
mmdb: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb',
|
mmdb: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb',
|
||||||
asn: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb'
|
asn: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/renderer/floating.html
Normal file
17
src/renderer/floating.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" lang="zh" />
|
||||||
|
<title>Mihomo Party Floating</title>
|
||||||
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data:; frame-src http://127.0.0.1:*;"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/floating.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { NavigateFunction, useLocation, useNavigate, useRoutes } from 'react-router-dom'
|
import { NavigateFunction, useLocation, useNavigate, useRoutes } from 'react-router-dom'
|
||||||
import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher'
|
import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher'
|
||||||
import SysproxySwitcher from '@renderer/components/sider/sysproxy-switcher'
|
import SysproxySwitcher from '@renderer/components/sider/sysproxy-switcher'
|
||||||
import TunSwitcher from '@renderer/components/sider/tun-switcher'
|
import TunSwitcher from '@renderer/components/sider/tun-switcher'
|
||||||
import { Button, Divider } from '@nextui-org/react'
|
import { Button, Divider } from '@heroui/react'
|
||||||
import { IoSettings } from 'react-icons/io5'
|
import { IoSettings } from 'react-icons/io5'
|
||||||
import routes from '@renderer/routes'
|
import routes from '@renderer/routes'
|
||||||
import {
|
import {
|
||||||
@ -35,15 +35,23 @@ import SubStoreCard from '@renderer/components/sider/substore-card'
|
|||||||
import MihomoIcon from './components/base/mihomo-icon'
|
import MihomoIcon from './components/base/mihomo-icon'
|
||||||
import { driver } from 'driver.js'
|
import { driver } from 'driver.js'
|
||||||
import 'driver.js/dist/driver.css'
|
import 'driver.js/dist/driver.css'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
let navigate: NavigateFunction
|
let navigate: NavigateFunction
|
||||||
|
let driverInstance: ReturnType<typeof driver> | null = null
|
||||||
|
|
||||||
|
export function getDriver(): ReturnType<typeof driver> | null {
|
||||||
|
return driverInstance
|
||||||
|
}
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const {
|
const {
|
||||||
appTheme = 'system',
|
appTheme = 'system',
|
||||||
customTheme,
|
customTheme,
|
||||||
useWindowFrame = false,
|
useWindowFrame = false,
|
||||||
|
siderWidth = 250,
|
||||||
siderOrder = [
|
siderOrder = [
|
||||||
'sysproxy',
|
'sysproxy',
|
||||||
'tun',
|
'tun',
|
||||||
@ -60,37 +68,223 @@ const App: React.FC = () => {
|
|||||||
'substore'
|
'substore'
|
||||||
]
|
]
|
||||||
} = appConfig || {}
|
} = appConfig || {}
|
||||||
|
const narrowWidth = platform === 'darwin' ? 70 : 60
|
||||||
const [order, setOrder] = useState(siderOrder)
|
const [order, setOrder] = useState(siderOrder)
|
||||||
|
const [siderWidthValue, setSiderWidthValue] = useState(siderWidth)
|
||||||
|
const siderWidthValueRef = useRef(siderWidthValue)
|
||||||
|
const [resizing, setResizing] = useState(false)
|
||||||
|
const resizingRef = useRef(resizing)
|
||||||
const sensors = useSensors(useSensor(PointerSensor))
|
const sensors = useSensors(useSensor(PointerSensor))
|
||||||
const { setTheme, systemTheme } = useTheme()
|
const { setTheme, systemTheme } = useTheme()
|
||||||
navigate = useNavigate()
|
navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const page = useRoutes(routes)
|
const page = useRoutes(routes)
|
||||||
const setTitlebar = (): void => {
|
const setTitlebar = (): void => {
|
||||||
if (!useWindowFrame) {
|
if (!useWindowFrame && platform !== 'darwin') {
|
||||||
const options = { height: 48 } as TitleBarOverlayOptions
|
const options = { height: 48 } as TitleBarOverlayOptions
|
||||||
try {
|
try {
|
||||||
if (platform !== 'darwin') {
|
|
||||||
options.color = window.getComputedStyle(document.documentElement).backgroundColor
|
options.color = window.getComputedStyle(document.documentElement).backgroundColor
|
||||||
options.symbolColor = window.getComputedStyle(document.documentElement).color
|
options.symbolColor = window.getComputedStyle(document.documentElement).color
|
||||||
}
|
|
||||||
setTitleBarOverlay(options)
|
setTitleBarOverlay(options)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
|
||||||
setOrder(siderOrder)
|
|
||||||
}, [siderOrder])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setOrder(siderOrder)
|
||||||
|
setSiderWidthValue(siderWidth)
|
||||||
|
}, [siderOrder, siderWidth])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
siderWidthValueRef.current = siderWidthValue
|
||||||
|
resizingRef.current = resizing
|
||||||
|
}, [siderWidthValue, resizing])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
driverInstance = driver({
|
||||||
|
showProgress: true,
|
||||||
|
nextBtnText: t('common.next'),
|
||||||
|
prevBtnText: t('common.prev'),
|
||||||
|
doneBtnText: t('common.done'),
|
||||||
|
progressText: '{{current}} / {{total}}',
|
||||||
|
overlayOpacity: 0.9,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
element: 'none',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.welcome.title'),
|
||||||
|
description: t('guide.welcome.description'),
|
||||||
|
side: 'over',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.side',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.sider.title'),
|
||||||
|
description: t('guide.sider.description'),
|
||||||
|
side: 'right',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.sysproxy-card',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.card.title'),
|
||||||
|
description: t('guide.card.description'),
|
||||||
|
side: 'right',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.main',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.main.title'),
|
||||||
|
description: t('guide.main.description'),
|
||||||
|
side: 'left',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.profile-card',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.profile.title'),
|
||||||
|
description: t('guide.profile.description'),
|
||||||
|
side: 'right',
|
||||||
|
align: 'start',
|
||||||
|
onNextClick: async (): Promise<void> => {
|
||||||
|
navigate('/profiles')
|
||||||
|
setTimeout(() => {
|
||||||
|
driverInstance?.moveNext()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.profiles-sticky',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.import.title'),
|
||||||
|
description: t('guide.import.description'),
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.substore-import',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.substore.title'),
|
||||||
|
description: t('guide.substore.description'),
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.new-profile',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.localProfile.title'),
|
||||||
|
description: t('guide.localProfile.description'),
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.sysproxy-card',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.sysproxy.title'),
|
||||||
|
description: t('guide.sysproxy.description'),
|
||||||
|
side: 'right',
|
||||||
|
align: 'start',
|
||||||
|
onNextClick: async (): Promise<void> => {
|
||||||
|
navigate('/sysproxy')
|
||||||
|
setTimeout(() => {
|
||||||
|
driverInstance?.moveNext()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.sysproxy-settings',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.sysproxySetting.title'),
|
||||||
|
description: t('guide.sysproxySetting.description'),
|
||||||
|
side: 'top',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.tun-card',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.tun.title'),
|
||||||
|
description: t('guide.tun.description'),
|
||||||
|
side: 'right',
|
||||||
|
align: 'start',
|
||||||
|
onNextClick: async (): Promise<void> => {
|
||||||
|
navigate('/tun')
|
||||||
|
setTimeout(() => {
|
||||||
|
driverInstance?.moveNext()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.tun-settings',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.tunSetting.title'),
|
||||||
|
description: t('guide.tunSetting.description'),
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.override-card',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.override.title'),
|
||||||
|
description: t('guide.override.description'),
|
||||||
|
side: 'right',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.dns-card',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.dns.title'),
|
||||||
|
description: t('guide.dns.description'),
|
||||||
|
side: 'right',
|
||||||
|
align: 'center',
|
||||||
|
onNextClick: async (): Promise<void> => {
|
||||||
|
navigate('/profiles')
|
||||||
|
setTimeout(() => {
|
||||||
|
driverInstance?.moveNext()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: 'none',
|
||||||
|
popover: {
|
||||||
|
title: t('guide.end.title'),
|
||||||
|
description: t('guide.end.description'),
|
||||||
|
side: 'top',
|
||||||
|
align: 'center',
|
||||||
|
onNextClick: async (): Promise<void> => {
|
||||||
|
navigate('/profiles')
|
||||||
|
setTimeout(() => {
|
||||||
|
driverInstance?.destroy()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const tourShown = window.localStorage.getItem('tourShown')
|
const tourShown = window.localStorage.getItem('tourShown')
|
||||||
if (!tourShown) {
|
if (!tourShown) {
|
||||||
window.localStorage.setItem('tourShown', 'true')
|
window.localStorage.setItem('tourShown', 'true')
|
||||||
firstDriver.drive()
|
driverInstance.drive()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [t])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNativeTheme(appTheme)
|
setNativeTheme(appTheme)
|
||||||
@ -104,6 +298,18 @@ const App: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}, [customTheme])
|
}, [customTheme])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('mouseup', onResizeEnd)
|
||||||
|
return (): void => window.removeEventListener('mouseup', onResizeEnd)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onResizeEnd = (): void => {
|
||||||
|
if (resizingRef.current) {
|
||||||
|
setResizing(false)
|
||||||
|
patchAppConfig({ siderWidth: siderWidthValueRef.current })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onDragEnd = async (event: DragEndEvent): Promise<void> => {
|
const onDragEnd = async (event: DragEndEvent): Promise<void> => {
|
||||||
const { active, over } = event
|
const { active, over } = event
|
||||||
if (over) {
|
if (over) {
|
||||||
@ -138,24 +344,74 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
sysproxy: <SysproxySwitcher key="sysproxy" />,
|
sysproxy: SysproxySwitcher,
|
||||||
tun: <TunSwitcher key="tun" />,
|
tun: TunSwitcher,
|
||||||
profile: <ProfileCard key="profile" />,
|
profile: ProfileCard,
|
||||||
proxy: <ProxyCard key="proxy" />,
|
proxy: ProxyCard,
|
||||||
mihomo: <MihomoCoreCard key="mihomo" />,
|
mihomo: MihomoCoreCard,
|
||||||
connection: <ConnCard key="connection" />,
|
connection: ConnCard,
|
||||||
dns: <DNSCard key="dns" />,
|
dns: DNSCard,
|
||||||
sniff: <SniffCard key="sniff" />,
|
sniff: SniffCard,
|
||||||
log: <LogCard key="log" />,
|
log: LogCard,
|
||||||
rule: <RuleCard key="rule" />,
|
rule: RuleCard,
|
||||||
resource: <ResourceCard key="resource" />,
|
resource: ResourceCard,
|
||||||
override: <OverrideCard key="override" />,
|
override: OverrideCard,
|
||||||
substore: <SubStoreCard key="substore" />
|
substore: SubStoreCard
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-[100vh] flex">
|
<div
|
||||||
<div className="side w-[250px] h-full overflow-y-auto no-scrollbar">
|
onMouseMove={(e) => {
|
||||||
|
if (!resizing) return
|
||||||
|
if (e.clientX <= 150) {
|
||||||
|
setSiderWidthValue(narrowWidth)
|
||||||
|
} else if (e.clientX <= 250) {
|
||||||
|
setSiderWidthValue(250)
|
||||||
|
} else if (e.clientX >= 400) {
|
||||||
|
setSiderWidthValue(400)
|
||||||
|
} else {
|
||||||
|
setSiderWidthValue(e.clientX)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`w-full h-[100vh] flex ${resizing ? 'cursor-ew-resize' : ''}`}
|
||||||
|
>
|
||||||
|
{siderWidthValue === narrowWidth ? (
|
||||||
|
<div style={{ width: `${narrowWidth}px` }} className="side h-full">
|
||||||
|
<div className="app-drag flex justify-center items-center z-40 bg-transparent h-[49px]">
|
||||||
|
{platform !== 'darwin' && (
|
||||||
|
<MihomoIcon className="h-[32px] leading-[32px] text-lg mx-[1px]" />
|
||||||
|
)}
|
||||||
|
<UpdaterButton iconOnly={true} />
|
||||||
|
</div>
|
||||||
|
<div className="h-[calc(100%-110px)] overflow-y-auto no-scrollbar">
|
||||||
|
<div className="h-full w-full flex flex-col gap-2">
|
||||||
|
{order.map((key: string) => {
|
||||||
|
const Component = componentMap[key]
|
||||||
|
if (!Component) return null
|
||||||
|
return <Component key={key} iconOnly={true} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex justify-center items-center h-[48px]">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="app-nodrag"
|
||||||
|
isIconOnly
|
||||||
|
color={location.pathname.includes('/settings') ? 'primary' : 'default'}
|
||||||
|
variant={location.pathname.includes('/settings') ? 'solid' : 'light'}
|
||||||
|
onPress={() => {
|
||||||
|
navigate('/settings')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IoSettings className="text-[20px]" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{ width: `${siderWidthValue}px` }}
|
||||||
|
className="side h-full overflow-y-auto no-scrollbar"
|
||||||
|
>
|
||||||
<div className="app-drag sticky top-0 z-40 backdrop-blur bg-transparent h-[49px]">
|
<div className="app-drag sticky top-0 z-40 backdrop-blur bg-transparent h-[49px]">
|
||||||
<div
|
<div
|
||||||
className={`flex justify-between p-2 ${!useWindowFrame && platform === 'darwin' ? 'ml-[60px]' : ''}`}
|
className={`flex justify-between p-2 ${!useWindowFrame && platform === 'darwin' ? 'ml-[60px]' : ''}`}
|
||||||
@ -174,8 +430,9 @@ const App: React.FC = () => {
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigate('/settings')
|
navigate('/settings')
|
||||||
}}
|
}}
|
||||||
startContent={<IoSettings className="text-[20px]" />}
|
>
|
||||||
/>
|
<IoSettings className="text-[20px]" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 mx-2">
|
<div className="mt-2 mx-2">
|
||||||
@ -185,204 +442,39 @@ const App: React.FC = () => {
|
|||||||
<div className="grid grid-cols-2 gap-2 m-2">
|
<div className="grid grid-cols-2 gap-2 m-2">
|
||||||
<SortableContext items={order}>
|
<SortableContext items={order}>
|
||||||
{order.map((key: string) => {
|
{order.map((key: string) => {
|
||||||
return componentMap[key]
|
const Component = componentMap[key]
|
||||||
|
if (!Component) return null
|
||||||
|
return <Component key={key} />
|
||||||
})}
|
})}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</div>
|
</div>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
onMouseDown={() => {
|
||||||
|
setResizing(true)
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: 50,
|
||||||
|
left: `${siderWidthValue - 2}px`,
|
||||||
|
width: '5px',
|
||||||
|
height: '100vh',
|
||||||
|
cursor: 'ew-resize'
|
||||||
|
}}
|
||||||
|
className={resizing ? 'bg-primary' : ''}
|
||||||
|
/>
|
||||||
<Divider orientation="vertical" />
|
<Divider orientation="vertical" />
|
||||||
<div className="main w-[calc(100%-251px)] h-full overflow-y-auto">{page}</div>
|
<div
|
||||||
|
style={{ width: `calc(100% - ${siderWidthValue + 1}px)` }}
|
||||||
|
className="main grow h-full overflow-y-auto"
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|
||||||
export const firstDriver = driver({
|
|
||||||
showProgress: true,
|
|
||||||
nextBtnText: '下一步',
|
|
||||||
prevBtnText: '上一步',
|
|
||||||
doneBtnText: '完成',
|
|
||||||
progressText: '{{current}} / {{total}}',
|
|
||||||
overlayOpacity: 0.9,
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
element: 'none',
|
|
||||||
popover: {
|
|
||||||
title: '欢迎使用 Mihomo Party',
|
|
||||||
description:
|
|
||||||
'这是一份交互式使用教程,如果您已经完全熟悉本软件的操作,可以直接点击右上角关闭按钮,后续您可以随时从设置中打开本教程',
|
|
||||||
side: 'over',
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.side',
|
|
||||||
popover: {
|
|
||||||
title: '导航栏',
|
|
||||||
description:
|
|
||||||
'左侧是应用的导航栏,兼顾仪表盘功能,在这里可以切换不同页面,也可以概览常用的状态信息',
|
|
||||||
side: 'right',
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.sysproxy-card',
|
|
||||||
popover: {
|
|
||||||
title: '卡片',
|
|
||||||
description: '点击导航栏卡片可以跳转到对应页面,拖动导航栏卡片可以自由排列卡片顺序',
|
|
||||||
side: 'right',
|
|
||||||
align: 'start'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.main',
|
|
||||||
popover: {
|
|
||||||
title: '主要区域',
|
|
||||||
description: '右侧是应用的主要区域,展示了导航栏所选页面的内容',
|
|
||||||
side: 'left',
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.profile-card',
|
|
||||||
popover: {
|
|
||||||
title: '订阅管理',
|
|
||||||
description:
|
|
||||||
'订阅管理卡片展示当前运行的订阅配置信息,点击进入订阅管理页面可以在这里管理订阅配置',
|
|
||||||
side: 'right',
|
|
||||||
align: 'start',
|
|
||||||
onNextClick: async (): Promise<void> => {
|
|
||||||
navigate('/profiles')
|
|
||||||
setTimeout(() => {
|
|
||||||
firstDriver.moveNext()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.profiles-sticky',
|
|
||||||
popover: {
|
|
||||||
title: '订阅导入',
|
|
||||||
description:
|
|
||||||
'Mihomo Party 支持多种订阅导入方式,在此输入订阅链接,点击导入即可导入您的订阅配置,如果您的订阅需要代理才能更新,请勾选“代理”再点击导入,当然这需要已经有一个可以正常使用的订阅才可以',
|
|
||||||
side: 'bottom',
|
|
||||||
align: 'start'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.substore-import',
|
|
||||||
popover: {
|
|
||||||
title: 'Sub-Store',
|
|
||||||
description:
|
|
||||||
'Mihomo Party 深度集成了 Sub-Store,您可以点击该按钮进入 Sub-Store 或直接导入您通过 Sub-Store 管理的订阅,Mihomo Party 默认使用内置的 Sub-Store 后端,如果您有自建的 Sub-Store 后端,可以在设置页面中配置,如果您不使用 Sub-Store 也可以在设置页面中关闭',
|
|
||||||
side: 'bottom',
|
|
||||||
align: 'start'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.new-profile',
|
|
||||||
popover: {
|
|
||||||
title: '本地订阅',
|
|
||||||
description: '点击“+”可以选择本地文件进行导入或者直接新建空白配置进行编辑',
|
|
||||||
side: 'bottom',
|
|
||||||
align: 'start'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.sysproxy-card',
|
|
||||||
popover: {
|
|
||||||
title: '系统代理',
|
|
||||||
description:
|
|
||||||
'导入订阅之后,内核已经开始运行并监听指定端口,此时您已经可以通过指定代理端口来使用代理了,如果您要使大部分应用自动使用该端口的代理,您还需要打开系统代理开关',
|
|
||||||
side: 'right',
|
|
||||||
align: 'start',
|
|
||||||
onNextClick: async (): Promise<void> => {
|
|
||||||
navigate('/sysproxy')
|
|
||||||
setTimeout(() => {
|
|
||||||
firstDriver.moveNext()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.sysproxy-settings',
|
|
||||||
popover: {
|
|
||||||
title: '系统代理设置',
|
|
||||||
description:
|
|
||||||
'在此您可以进行系统代理相关设置,选择代理模式,如果某些 Windows 应用不遵循系统代理,还可以使用“UWP 工具”解除本地回环限制,对于“手动代理模式”和“PAC 代理模式”的区别,请自行百度',
|
|
||||||
side: 'top',
|
|
||||||
align: 'start'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.tun-card',
|
|
||||||
popover: {
|
|
||||||
title: '虚拟网卡',
|
|
||||||
description:
|
|
||||||
'虚拟网卡,即同类软件中常见的“Tun 模式”,对于某些不遵循系统代理的应用,您可以打开虚拟网卡以让内核接管所有流量',
|
|
||||||
side: 'right',
|
|
||||||
align: 'start',
|
|
||||||
onNextClick: async (): Promise<void> => {
|
|
||||||
navigate('/tun')
|
|
||||||
setTimeout(() => {
|
|
||||||
firstDriver.moveNext()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.tun-settings',
|
|
||||||
popover: {
|
|
||||||
title: '虚拟网卡设置',
|
|
||||||
description:
|
|
||||||
'这里可以更改虚拟网卡相关设置,Mihomo Party 理论上已经完全解决权限问题,如果您的虚拟网卡仍然不可用,可以尝试重设防火墙(Windows)或手动授权内核(MacOS/Linux)后重启内核',
|
|
||||||
side: 'bottom',
|
|
||||||
align: 'start'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.override-card',
|
|
||||||
popover: {
|
|
||||||
title: '覆写',
|
|
||||||
description:
|
|
||||||
'Mihomo Party 提供强大的覆写功能,可以对您导入的订阅配置进行个性化修改,如添加规则、自定义代理组等,您可以直接导入别人写好的覆写文件,也可以自己动手编写,<b>编辑好覆写文件一定要记得在需要覆写的订阅上启用</b>,覆写文件的语法请参考 <a href="https://mihomo.party/docs/guide/override" target="_blank">官方文档</a>',
|
|
||||||
side: 'right',
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: '.dns-card',
|
|
||||||
popover: {
|
|
||||||
title: 'DNS',
|
|
||||||
description:
|
|
||||||
'软件默认接管了内核的 DNS 设置,如果您需要使用订阅配置中的 DNS 设置,可以到应用设置中关闭“接管 DNS 设置”,域名嗅探同理',
|
|
||||||
side: 'right',
|
|
||||||
align: 'center',
|
|
||||||
onNextClick: async (): Promise<void> => {
|
|
||||||
navigate('/profiles')
|
|
||||||
setTimeout(() => {
|
|
||||||
firstDriver.moveNext()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: 'none',
|
|
||||||
popover: {
|
|
||||||
title: '教程结束',
|
|
||||||
description:
|
|
||||||
'现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 <a href="https://t.me/mihomo_party_channel" target="_blank">Telegram 频道</a> 获取最新资讯',
|
|
||||||
side: 'top',
|
|
||||||
align: 'center',
|
|
||||||
onNextClick: async (): Promise<void> => {
|
|
||||||
navigate('/profiles')
|
|
||||||
setTimeout(() => {
|
|
||||||
firstDriver.destroy()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|||||||
101
src/renderer/src/FloatingApp.tsx
Normal file
101
src/renderer/src/FloatingApp.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import MihomoIcon from './components/base/mihomo-icon'
|
||||||
|
import { calcTraffic } from './utils/calc'
|
||||||
|
import { showContextMenu, triggerMainWindow } from './utils/ipc'
|
||||||
|
import { useAppConfig } from './hooks/use-app-config'
|
||||||
|
import { useControledMihomoConfig } from './hooks/use-controled-mihomo-config'
|
||||||
|
|
||||||
|
const FloatingApp: React.FC = () => {
|
||||||
|
const { appConfig } = useAppConfig()
|
||||||
|
const { controledMihomoConfig } = useControledMihomoConfig()
|
||||||
|
const { sysProxy, spinFloatingIcon = true } = appConfig || {}
|
||||||
|
const { tun } = controledMihomoConfig || {}
|
||||||
|
const sysProxyEnabled = sysProxy?.enable
|
||||||
|
const tunEnabled = tun?.enable
|
||||||
|
|
||||||
|
const [upload, setUpload] = useState(0)
|
||||||
|
const [download, setDownload] = useState(0)
|
||||||
|
|
||||||
|
// 根据总速率计算旋转速度
|
||||||
|
const spinSpeed = useMemo(() => {
|
||||||
|
const total = upload + download
|
||||||
|
if (total === 0) return 0
|
||||||
|
if (total < 1024) return 2
|
||||||
|
if (total < 1024 * 1024) return 3
|
||||||
|
if (total < 1024 * 1024 * 1024) return 4
|
||||||
|
return 5
|
||||||
|
}, [upload, download])
|
||||||
|
|
||||||
|
const [rotation, setRotation] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!spinFloatingIcon) return
|
||||||
|
|
||||||
|
let animationFrameId: number
|
||||||
|
const animate = (): void => {
|
||||||
|
setRotation((prev) => {
|
||||||
|
if (prev === 360) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return prev + spinSpeed
|
||||||
|
})
|
||||||
|
animationFrameId = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(animate)
|
||||||
|
return (): void => {
|
||||||
|
cancelAnimationFrame(animationFrameId)
|
||||||
|
}
|
||||||
|
}, [spinSpeed, spinFloatingIcon])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
|
||||||
|
setUpload(info.up)
|
||||||
|
setDownload(info.down)
|
||||||
|
})
|
||||||
|
return (): void => {
|
||||||
|
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-drag h-[100vh] w-[100vw] overflow-hidden">
|
||||||
|
<div className="floating-bg border-1 border-divider flex rounded-full bg-content1 h-[calc(100%-2px)] w-[calc(100%-2px)]">
|
||||||
|
<div className="flex justify-center items-center h-[100%] aspect-square">
|
||||||
|
<div
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
showContextMenu()
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
triggerMainWindow()
|
||||||
|
}}
|
||||||
|
style={
|
||||||
|
spinFloatingIcon
|
||||||
|
? {
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
|
transition: 'transform 0.1s linear'
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
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`}
|
||||||
|
>
|
||||||
|
<MihomoIcon className="floating-icon text-primary-foreground h-full leading-full text-[22px] mx-auto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full overflow-hidden">
|
||||||
|
<div className="flex flex-col justify-center h-full w-full">
|
||||||
|
<h2 className="text-end floating-text whitespace-nowrap text-[12px] mr-2 font-bold">
|
||||||
|
{calcTraffic(upload)}/s
|
||||||
|
</h2>
|
||||||
|
<h2 className="text-end floating-text whitespace-nowrap text-[12px] mr-2 font-bold">
|
||||||
|
{calcTraffic(download)}/s
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FloatingApp
|
||||||
28
src/renderer/src/assets/floating.css
Normal file
28
src/renderer/src/assets/floating.css
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.floating-text {
|
||||||
|
font-family:
|
||||||
|
'Microsoft YaHei',
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: none !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-nodrag {
|
||||||
|
-webkit-app-region: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-drag {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
@ -8,22 +8,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover {
|
.driver-popover {
|
||||||
background-color: hsl(var(--nextui-content2)) !important;
|
background-color: hsl(var(--heroui-content2)) !important;
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
color: hsl(var(--nextui-foreground)) !important;
|
color: hsl(var(--heroui-foreground)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover a {
|
.driver-popover a {
|
||||||
color: hsl(var(--nextui-primary)) !important;
|
color: hsl(var(--heroui-primary)) !important;
|
||||||
text-decoration: underline !important;
|
text-decoration: underline !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-close-btn {
|
.driver-popover-close-btn {
|
||||||
color: hsl(var(--nextui-foreground)) !important;
|
color: hsl(var(--heroui-foreground)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-progress-text {
|
.driver-popover-progress-text {
|
||||||
color: hsl(var(--nextui-default-500)) !important;
|
color: hsl(var(--heroui-default-500)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-prev-btn {
|
.driver-popover-prev-btn {
|
||||||
@ -33,7 +33,7 @@
|
|||||||
padding: 8px !important;
|
padding: 8px !important;
|
||||||
border-radius: 5px !important;
|
border-radius: 5px !important;
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
background-color: hsl(var(--nextui-primary)) !important;
|
background-color: hsl(var(--heroui-primary)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-next-btn {
|
.driver-popover-next-btn {
|
||||||
@ -43,23 +43,23 @@
|
|||||||
padding: 8px !important;
|
padding: 8px !important;
|
||||||
border-radius: 5px !important;
|
border-radius: 5px !important;
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
background-color: hsl(var(--nextui-primary)) !important;
|
background-color: hsl(var(--heroui-primary)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-arrow-side-bottom {
|
.driver-popover-arrow-side-bottom {
|
||||||
border-bottom-color: hsl(var(--nextui-content2)) !important;
|
border-bottom-color: hsl(var(--heroui-content2)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-arrow-side-top {
|
.driver-popover-arrow-side-top {
|
||||||
border-top-color: hsl(var(--nextui-content2)) !important;
|
border-top-color: hsl(var(--heroui-content2)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-arrow-side-left {
|
.driver-popover-arrow-side-left {
|
||||||
border-left-color: hsl(var(--nextui-content2)) !important;
|
border-left-color: hsl(var(--heroui-content2)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-arrow-side-right {
|
.driver-popover-arrow-side-right {
|
||||||
border-right-color: hsl(var(--nextui-content2)) !important;
|
border-right-color: hsl(var(--heroui-content2)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-nodrag {
|
.app-nodrag {
|
||||||
|
|||||||
37
src/renderer/src/components/base/base-confirm-modal.tsx
Normal file
37
src/renderer/src/components/base/base-confirm-modal.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
onCancel: () => void
|
||||||
|
onConfirm: () => void
|
||||||
|
isOpen: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseConfirmModal: React.FC<Props> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { title, content, onCancel, onConfirm, isOpen } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal backdrop="blur" classNames={{ backdrop: 'top-[48px]' }} hideCloseButton isOpen={isOpen}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader className="flex app-drag">{title}</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<p>{content}</p>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button size="sm" variant="light" onPress={onCancel}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" color="danger" onPress={onConfirm}>
|
||||||
|
{t('common.confirm')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseConfirmModal
|
||||||
@ -7,7 +7,7 @@ import pac from 'types-pac/pac.d.ts?raw'
|
|||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
type Language = 'yaml' | 'javascript' | 'css'
|
type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string
|
value: string
|
||||||
@ -89,7 +89,7 @@ export const BaseEditor: React.FC<Props> = (props) => {
|
|||||||
const trueTheme = theme === 'system' ? systemTheme : theme
|
const trueTheme = theme === 'system' ? systemTheme : theme
|
||||||
const { value, readOnly = false, language, onChange } = props
|
const { value, readOnly = false, language, onChange } = props
|
||||||
|
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>()
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(undefined)
|
||||||
|
|
||||||
const editorWillMount = (): void => {
|
const editorWillMount = (): void => {
|
||||||
monacoInitialization()
|
monacoInitialization()
|
||||||
@ -105,7 +105,9 @@ export const BaseEditor: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.onresize = (): void => {
|
window.onresize = (): void => {
|
||||||
|
setTimeout(() => {
|
||||||
editorRef.current?.layout()
|
editorRef.current?.layout()
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
return (): void => {
|
return (): void => {
|
||||||
window.onresize = null
|
window.onresize = null
|
||||||
@ -139,6 +141,7 @@ export const BaseEditor: React.FC<Props> = (props) => {
|
|||||||
}}
|
}}
|
||||||
editorWillMount={editorWillMount}
|
editorWillMount={editorWillMount}
|
||||||
editorDidMount={editorDidMount}
|
editorDidMount={editorDidMount}
|
||||||
|
editorWillUnmount={(): void => { }}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import { Button } from '@nextui-org/react'
|
import { Button } from '@heroui/react'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const ErrorFallback = ({ error }: FallbackProps): JSX.Element => {
|
const ErrorFallback = ({ error }: FallbackProps): JSX.Element => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h2 className="my-2 text-lg font-bold">
|
<h2 className="my-2 text-lg font-bold">
|
||||||
{'应用崩溃了 :( 请将以下信息提交给开发者以排查错误'}
|
{t('common.error.appCrash')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -22,7 +25,7 @@ const ErrorFallback = ({ error }: FallbackProps): JSX.Element => {
|
|||||||
color="primary"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
onPress={() => open('https://t.me/mihomo_party')}
|
onPress={() => open('https://t.me/mihomo_party_group')}
|
||||||
>
|
>
|
||||||
Telegram
|
Telegram
|
||||||
</Button>
|
</Button>
|
||||||
@ -35,7 +38,7 @@ const ErrorFallback = ({ error }: FallbackProps): JSX.Element => {
|
|||||||
navigator.clipboard.writeText('```\n' + error.message + '\n' + error.stack + '\n```')
|
navigator.clipboard.writeText('```\n' + error.message + '\n' + error.stack + '\n```')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
复制报错信息
|
{t('common.error.copyErrorMessage')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p className="my-2">{error.message}</p>
|
<p className="my-2">{error.message}</p>
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { Button, Divider } from '@nextui-org/react'
|
import { Button, Divider } from '@heroui/react'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
import { platform } from '@renderer/utils/init'
|
import { platform } from '@renderer/utils/init'
|
||||||
import { isAlwaysOnTop, setAlwaysOnTop } from '@renderer/utils/ipc'
|
import { isAlwaysOnTop, setAlwaysOnTop } from '@renderer/utils/ipc'
|
||||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||||
import { RiPushpin2Fill, RiPushpin2Line } from 'react-icons/ri'
|
import { RiPushpin2Fill, RiPushpin2Line } from 'react-icons/ri'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: React.ReactNode
|
title?: React.ReactNode
|
||||||
header?: React.ReactNode
|
header?: React.ReactNode
|
||||||
@ -13,6 +15,7 @@ interface Props {
|
|||||||
let saveOnTop = false
|
let saveOnTop = false
|
||||||
|
|
||||||
const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { appConfig } = useAppConfig()
|
const { appConfig } = useAppConfig()
|
||||||
const { useWindowFrame = false } = appConfig || {}
|
const { useWindowFrame = false } = appConfig || {}
|
||||||
const [overlayWidth, setOverlayWidth] = React.useState(0)
|
const [overlayWidth, setOverlayWidth] = React.useState(0)
|
||||||
@ -51,7 +54,7 @@ const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="app-nodrag"
|
className="app-nodrag"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
title="窗口置顶"
|
title={t('common.pinWindow')}
|
||||||
variant="light"
|
variant="light"
|
||||||
color={onTop ? 'primary' : 'default'}
|
color={onTop ? 'primary' : 'default'}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
Button,
|
|
||||||
Input
|
|
||||||
} from '@nextui-org/react'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onCancel: () => void
|
|
||||||
onConfirm: (script: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const BasePasswordModal: React.FC<Props> = (props) => {
|
|
||||||
const { onCancel, onConfirm } = props
|
|
||||||
const [password, setPassword] = useState('')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal backdrop="blur" classNames={{ backdrop: 'top-[48px]' }} hideCloseButton isOpen={true}>
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader className="flex">请输入root密码</ModalHeader>
|
|
||||||
<ModalBody>
|
|
||||||
<Input fullWidth type="password" value={password} onValueChange={setPassword} />
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button variant="light" onPress={onCancel}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" onPress={() => onConfirm(password)}>
|
|
||||||
确认
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BasePasswordModal
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Accordion, AccordionItem, Card, CardBody } from '@nextui-org/react'
|
import { Accordion, AccordionItem, Card, CardBody } from '@heroui/react'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string
|
title?: string
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Divider } from '@nextui-org/react'
|
import { Divider } from '@heroui/react'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ const SettingItem: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-[32px] w-full flex justify-between">
|
<div className="select-text h-[32px] w-full flex justify-between">
|
||||||
<div className="h-full flex items-center">
|
<div className="h-full flex items-center">
|
||||||
<h4 className="h-full text-md leading-[32px] whitespace-nowrap">{title}</h4>
|
<h4 className="h-full text-md leading-[32px] whitespace-nowrap">{title}</h4>
|
||||||
<div>{actions}</div>
|
<div>{actions}</div>
|
||||||
|
|||||||
5
src/renderer/src/components/base/border-switch.css
Normal file
5
src/renderer/src/components/base/border-switch.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.border-switch {
|
||||||
|
input[type='checkbox'] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,24 +1,28 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { cn, Switch, SwitchProps } from '@nextui-org/react'
|
import { cn, Switch, SwitchProps } from '@heroui/react'
|
||||||
|
import './border-switch.css'
|
||||||
|
|
||||||
interface SiderSwitchProps extends SwitchProps {
|
interface BorderSwitchProps extends Omit<SwitchProps, 'isSelected'> {
|
||||||
isShowBorder?: boolean
|
isShowBorder?: boolean
|
||||||
|
isSelected?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const BorderSwitch: React.FC<SiderSwitchProps> = (props) => {
|
const BorderSwitch: React.FC<BorderSwitchProps> = (props) => {
|
||||||
const { isShowBorder = false, classNames, ...switchProps } = props
|
const { isShowBorder = false, isSelected = false, classNames, ...switchProps } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
|
className="border-switch px-[8px]"
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: cn('border-2', {
|
wrapper: cn('border-2', {
|
||||||
'border-transparent': !isShowBorder,
|
'border-transparent': !isShowBorder,
|
||||||
'border-white': isShowBorder
|
'border-primary-foreground': isShowBorder
|
||||||
}),
|
}),
|
||||||
thumb: cn('absolute z-4', 'transform -translate-x-[2px]'),
|
thumb: cn('absolute z-4', 'transform -translate-x-[2px]'),
|
||||||
...classNames
|
...classNames
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
isSelected={isSelected}
|
||||||
{...switchProps}
|
{...switchProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
import { Input, InputProps } from '@nextui-org/react'
|
import { Input, InputProps } from '@heroui/react'
|
||||||
import { FaSearch } from 'react-icons/fa'
|
import { FaSearch } from 'react-icons/fa'
|
||||||
|
|
||||||
interface CollapseInputProps extends InputProps {
|
interface CollapseInputProps extends InputProps {
|
||||||
@ -22,7 +22,7 @@ const CollapseInput: React.FC<CollapseInputProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
endContent={
|
endContent={
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer p-2 text-lg text-default-500"
|
className="cursor-pointer p-2 text-lg text-foreground-500"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
@ -31,7 +31,7 @@ const CollapseInput: React.FC<CollapseInputProps> = (props) => {
|
|||||||
<FaSearch title={title} />
|
<FaSearch title={title} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onClick={(e) => {
|
onPress={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,15 +1,129 @@
|
|||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
DropdownTrigger,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownItem
|
||||||
|
} from '@heroui/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { calcTraffic } from '@renderer/utils/calc'
|
import { calcTraffic } from '@renderer/utils/calc'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from '@renderer/utils/dayjs'
|
||||||
|
import { BiCopy } from 'react-icons/bi'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connection: IMihomoConnectionDetail
|
connection: IMihomoConnectionDetail
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CopyableSettingItem: React.FC<{
|
||||||
|
title: string
|
||||||
|
value: string | string[]
|
||||||
|
displayName?: string
|
||||||
|
prefix?: string[]
|
||||||
|
}> = ({ title, value, displayName, prefix = [] }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const getSubDomains = (domain: string): string[] =>
|
||||||
|
domain.split('.').length <= 2
|
||||||
|
? [domain]
|
||||||
|
: domain
|
||||||
|
.split('.')
|
||||||
|
.map((_, i, parts) => parts.slice(i).join('.'))
|
||||||
|
.slice(0, -1)
|
||||||
|
const isIPv6 = (ip: string) => ip.includes(':')
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ key: 'raw', text: displayName || (Array.isArray(value) ? value.join(', ') : value) },
|
||||||
|
...(Array.isArray(value)
|
||||||
|
? value.map((v, i) => {
|
||||||
|
const p = prefix[i]
|
||||||
|
if (!p || !v) return null
|
||||||
|
|
||||||
|
if (p === 'DOMAIN-SUFFIX') {
|
||||||
|
return getSubDomains(v).map((subV) => ({
|
||||||
|
key: `${p},${subV}`,
|
||||||
|
text: `${p},${subV}`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p === 'IP-ASN' || p === 'SRC-IP-ASN') {
|
||||||
|
return {
|
||||||
|
key: `${p},${v.split(' ')[0]}`,
|
||||||
|
text: `${p},${v.split(' ')[0]}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix = (p === 'IP-CIDR' || p === 'SRC-IP-CIDR') ? (isIPv6(v) ? '/128' : '/32') : ''
|
||||||
|
return {
|
||||||
|
key: `${p},${v}${suffix}`,
|
||||||
|
text: `${p},${v}${suffix}`
|
||||||
|
}
|
||||||
|
}).filter(Boolean).flat()
|
||||||
|
: prefix.map(p => {
|
||||||
|
const v = value as string
|
||||||
|
if (p === 'DOMAIN-SUFFIX') {
|
||||||
|
return getSubDomains(v).map((subV) => ({
|
||||||
|
key: `${p},${subV}`,
|
||||||
|
text: `${p},${subV}`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p === 'IP-ASN' || p === 'SRC-IP-ASN') {
|
||||||
|
return {
|
||||||
|
key: `${p},${v.split(' ')[0]}`,
|
||||||
|
text: `${p},${v.split(' ')[0]}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix = (p === 'IP-CIDR' || p === 'SRC-IP-CIDR') ? (isIPv6(v) ? '/128' : '/32') : ''
|
||||||
|
return {
|
||||||
|
key: `${p},${v}${suffix}`,
|
||||||
|
text: `${p},${v}${suffix}`
|
||||||
|
}
|
||||||
|
}).flat())
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingItem
|
||||||
|
title={title}
|
||||||
|
actions={
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button title={t('connections.detail.copyRule')} isIconOnly size="sm" variant="light">
|
||||||
|
<BiCopy className="text-lg" />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
onAction={(key) =>
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
key === 'raw' ? (Array.isArray(value) ? value.join(', ') : value) : (key as string)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{menuItems
|
||||||
|
.filter((item) => item !== null)
|
||||||
|
.map(({ key, text }) => (
|
||||||
|
<DropdownItem key={key}>{text}</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{displayName || (Array.isArray(value) ? value.join(', ') : value)}
|
||||||
|
</SettingItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ConnectionDetailModal: React.FC<Props> = (props) => {
|
const ConnectionDetailModal: React.FC<Props> = (props) => {
|
||||||
const { connection, onClose } = props
|
const { connection, onClose } = props
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
@ -21,83 +135,164 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
|
|||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
>
|
>
|
||||||
<ModalContent className="flag-emoji break-all">
|
<ModalContent className="flag-emoji break-all">
|
||||||
<ModalHeader className="flex">连接详情</ModalHeader>
|
<ModalHeader className="flex app-drag">{t('connections.detail.title')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<SettingItem title="连接类型">
|
<SettingItem title={t('connections.detail.establishTime')}>{dayjs(connection.start).fromNow()}</SettingItem>
|
||||||
{connection.metadata.type}({connection.metadata.network})
|
<SettingItem title={t('connections.detail.rule')}>
|
||||||
</SettingItem>
|
|
||||||
<SettingItem title="连接建立时间">{dayjs(connection.start).fromNow()}</SettingItem>
|
|
||||||
<SettingItem title="规则">
|
|
||||||
{connection.rule}
|
{connection.rule}
|
||||||
{connection.rulePayload ? `(${connection.rulePayload})` : ''}
|
{connection.rulePayload ? `(${connection.rulePayload})` : ''}
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="代理链">{[...connection.chains].reverse().join('>>')}</SettingItem>
|
<SettingItem title={t('connections.detail.proxyChain')}>{[...connection.chains].reverse().join('>>')}</SettingItem>
|
||||||
<SettingItem title="上传速度">{calcTraffic(connection.uploadSpeed || 0)}/s</SettingItem>
|
<SettingItem title={t('connections.uploadSpeed')}>{calcTraffic(connection.uploadSpeed || 0)}/s</SettingItem>
|
||||||
<SettingItem title="下载速度">{calcTraffic(connection.downloadSpeed || 0)}/s</SettingItem>
|
<SettingItem title={t('connections.downloadSpeed')}>{calcTraffic(connection.downloadSpeed || 0)}/s</SettingItem>
|
||||||
<SettingItem title="上传量">{calcTraffic(connection.upload)}</SettingItem>
|
<SettingItem title={t('connections.uploadAmount')}>{calcTraffic(connection.upload)}</SettingItem>
|
||||||
<SettingItem title="下载量">{calcTraffic(connection.download)}</SettingItem>
|
<SettingItem title={t('connections.downloadAmount')}>{calcTraffic(connection.download)}</SettingItem>
|
||||||
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.connectionType')}
|
||||||
|
value={[connection.metadata.type, connection.metadata.network]}
|
||||||
|
displayName={`${connection.metadata.type}(${connection.metadata.network})`}
|
||||||
|
prefix={['IN-TYPE', 'NETWORK']}
|
||||||
|
/>
|
||||||
|
{connection.metadata.host && (
|
||||||
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.host')}
|
||||||
|
value={connection.metadata.host}
|
||||||
|
prefix={['DOMAIN', 'DOMAIN-SUFFIX']}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{connection.metadata.sniffHost && (
|
||||||
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.sniffHost')}
|
||||||
|
value={connection.metadata.sniffHost}
|
||||||
|
prefix={['DOMAIN', 'DOMAIN-SUFFIX']}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{connection.metadata.process && (
|
{connection.metadata.process && (
|
||||||
<SettingItem title="进程名">
|
<CopyableSettingItem
|
||||||
{connection.metadata.process}
|
title={t('connections.detail.processName')}
|
||||||
{connection.metadata.uid ? `(${connection.metadata.uid})` : ''}
|
value={[
|
||||||
</SettingItem>
|
connection.metadata.process,
|
||||||
|
...(connection.metadata.uid ? [connection.metadata.uid.toString()] : [])
|
||||||
|
]}
|
||||||
|
displayName={`${connection.metadata.process}${connection.metadata.uid ? `(${connection.metadata.uid})` : ''}`}
|
||||||
|
prefix={['PROCESS-NAME', ...(connection.metadata.uid ? ['UID'] : [])]}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.processPath && (
|
{connection.metadata.processPath && (
|
||||||
<SettingItem title="进程路径">{connection.metadata.processPath}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.processPath')}
|
||||||
|
value={connection.metadata.processPath}
|
||||||
|
prefix={['PROCESS-PATH']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.sourceIP && (
|
{connection.metadata.sourceIP && (
|
||||||
<SettingItem title="源IP">{connection.metadata.sourceIP}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.sourceIP')}
|
||||||
|
value={connection.metadata.sourceIP}
|
||||||
|
prefix={['SRC-IP-CIDR']}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{connection.metadata.sourceGeoIP && connection.metadata.sourceGeoIP.length > 0 && (
|
||||||
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.sourceGeoIP')}
|
||||||
|
value={connection.metadata.sourceGeoIP}
|
||||||
|
prefix={['SRC-GEOIP']}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{connection.metadata.sourceIPASN && (
|
||||||
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.sourceASN')}
|
||||||
|
value={connection.metadata.sourceIPASN}
|
||||||
|
prefix={['SRC-IP-ASN']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.destinationIP && (
|
{connection.metadata.destinationIP && (
|
||||||
<SettingItem title="目标IP">{connection.metadata.destinationIP}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.destinationIP')}
|
||||||
|
value={connection.metadata.destinationIP}
|
||||||
|
prefix={['IP-CIDR']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.destinationGeoIP && (
|
{connection.metadata.destinationGeoIP &&
|
||||||
<SettingItem title="目标GeoIP">{connection.metadata.destinationGeoIP}</SettingItem>
|
connection.metadata.destinationGeoIP.length > 0 && (
|
||||||
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.destinationGeoIP')}
|
||||||
|
value={connection.metadata.destinationGeoIP}
|
||||||
|
prefix={['GEOIP']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.destinationIPASN && (
|
{connection.metadata.destinationIPASN && (
|
||||||
<SettingItem title="目标ASN">{connection.metadata.destinationIPASN}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.destinationASN')}
|
||||||
|
value={connection.metadata.destinationIPASN}
|
||||||
|
prefix={['IP-ASN']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.sourcePort && (
|
{connection.metadata.sourcePort && (
|
||||||
<SettingItem title="源端口">{connection.metadata.sourcePort}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.sourcePort')}
|
||||||
|
value={connection.metadata.sourcePort}
|
||||||
|
prefix={['SRC-PORT']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.destinationPort && (
|
{connection.metadata.destinationPort && (
|
||||||
<SettingItem title="目标端口">{connection.metadata.destinationPort}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.destinationPort')}
|
||||||
|
value={connection.metadata.destinationPort}
|
||||||
|
prefix={['DST-PORT']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.inboundIP && (
|
{connection.metadata.inboundIP && (
|
||||||
<SettingItem title="入站IP">{connection.metadata.inboundIP}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.inboundIP')}
|
||||||
|
value={connection.metadata.inboundIP}
|
||||||
|
prefix={['SRC-IP-CIDR']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.inboundPort && (
|
{connection.metadata.inboundPort && (
|
||||||
<SettingItem title="入站端口">{connection.metadata.inboundPort}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.inboundPort')}
|
||||||
|
value={connection.metadata.inboundPort}
|
||||||
|
prefix={['IN-PORT']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.inboundName && (
|
{connection.metadata.inboundName && (
|
||||||
<SettingItem title="入站名称">{connection.metadata.inboundName}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.inboundName')}
|
||||||
|
value={connection.metadata.inboundName}
|
||||||
|
prefix={['IN-NAME']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.inboundUser && (
|
{connection.metadata.inboundUser && (
|
||||||
<SettingItem title="入站用户">{connection.metadata.inboundUser}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.inboundUser')}
|
||||||
|
value={connection.metadata.inboundUser}
|
||||||
|
prefix={['IN-USER']}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.host && (
|
|
||||||
<SettingItem title="主机">{connection.metadata.host}</SettingItem>
|
<CopyableSettingItem
|
||||||
|
title={t('connections.detail.dscp')}
|
||||||
|
value={connection.metadata.dscp.toString()}
|
||||||
|
prefix={['DSCP']}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{connection.metadata.remoteDestination && (
|
||||||
|
<SettingItem title={t('connections.detail.remoteDestination')}>{connection.metadata.remoteDestination}</SettingItem>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.dnsMode && (
|
{connection.metadata.dnsMode && (
|
||||||
<SettingItem title="DNS模式">{connection.metadata.dnsMode}</SettingItem>
|
<SettingItem title={t('connections.detail.dnsMode')}>{connection.metadata.dnsMode}</SettingItem>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.specialProxy && (
|
{connection.metadata.specialProxy && (
|
||||||
<SettingItem title="特殊代理">{connection.metadata.specialProxy}</SettingItem>
|
<SettingItem title={t('connections.detail.specialProxy')}>{connection.metadata.specialProxy}</SettingItem>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.specialRules && (
|
{connection.metadata.specialRules && (
|
||||||
<SettingItem title="特殊规则">{connection.metadata.specialRules}</SettingItem>
|
<SettingItem title={t('connections.detail.specialRules')}>{connection.metadata.specialRules}</SettingItem>
|
||||||
)}
|
|
||||||
{connection.metadata.remoteDestination && (
|
|
||||||
<SettingItem title="远程目标">{connection.metadata.remoteDestination}</SettingItem>
|
|
||||||
)}
|
|
||||||
<SettingItem title="DSCP">{connection.metadata.dscp}</SettingItem>
|
|
||||||
{connection.metadata.sniffHost && (
|
|
||||||
<SettingItem title="嗅探主机">{connection.metadata.sniffHost}</SettingItem>
|
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="light" onPress={onClose}>
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
关闭
|
{t('connections.detail.close')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Button, Card, CardFooter, CardHeader, Chip } from '@nextui-org/react'
|
import { Button, Card, CardFooter, CardHeader, Chip } from '@heroui/react'
|
||||||
import { calcTraffic } from '@renderer/utils/calc'
|
import { calcTraffic } from '@renderer/utils/calc'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from '@renderer/utils/dayjs'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { CgClose, CgTrash } from 'react-icons/cg'
|
import { CgClose, CgTrash } from 'react-icons/cg'
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`px-2 pb-2 ${index === 0 ? 'pt-2' : ''}`}>
|
<div className={`px-2 pb-2 ${index === 0 ? 'pt-2' : ''}`}>
|
||||||
|
<div className="relative">
|
||||||
<Card
|
<Card
|
||||||
isPressable
|
isPressable
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@ -32,10 +33,15 @@ const ConnectionItem: React.FC<Props> = (props) => {
|
|||||||
setIsDetailModalOpen(true)
|
setIsDetailModalOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full flex justify-between">
|
<div className="w-full">
|
||||||
<div className="w-[calc(100%-48px)]">
|
<div className="w-full pr-12">
|
||||||
<CardHeader className="pb-0 gap-1">
|
<CardHeader className="pb-0 gap-1">
|
||||||
<Chip color={`${info.isActive ? "primary": "danger"}`} size="sm" radius="sm" variant="dot">
|
<Chip
|
||||||
|
color={`${info.isActive ? 'primary' : 'danger'}`}
|
||||||
|
size="sm"
|
||||||
|
radius="sm"
|
||||||
|
variant="dot"
|
||||||
|
>
|
||||||
{info.metadata.type}({info.metadata.network.toUpperCase()})
|
{info.metadata.type}({info.metadata.network.toUpperCase()})
|
||||||
</Chip>
|
</Chip>
|
||||||
<div className="text-ellipsis whitespace-nowrap overflow-hidden">
|
<div className="text-ellipsis whitespace-nowrap overflow-hidden">
|
||||||
@ -46,7 +52,7 @@ const ConnectionItem: React.FC<Props> = (props) => {
|
|||||||
info.metadata.destinationIP ||
|
info.metadata.destinationIP ||
|
||||||
info.metadata.remoteDestination}
|
info.metadata.remoteDestination}
|
||||||
</div>
|
</div>
|
||||||
<small className="whitespace-nowrap text-default-500">
|
<small className="whitespace-nowrap text-foreground-500">
|
||||||
{dayjs(info.start).fromNow()}
|
{dayjs(info.start).fromNow()}
|
||||||
</small>
|
</small>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -75,19 +81,20 @@ const ConnectionItem: React.FC<Props> = (props) => {
|
|||||||
) : null}
|
) : null}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
<Button
|
<Button
|
||||||
color={`${info.isActive ? "warning" : "danger"}`}
|
color={`${info.isActive ? 'warning' : 'danger'}`}
|
||||||
variant="light"
|
variant="light"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
className="mr-2 my-auto"
|
className="absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
close(info.id)
|
close(info.id)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{info.isActive ? (<CgClose className="text-lg"/>) : (<CgTrash className="text-lg"/>)}
|
{info.isActive ? <CgClose className="text-lg" /> : <CgTrash className="text-lg" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Card, CardBody, CardHeader } from '@nextui-org/react'
|
import { Card, CardBody, CardHeader } from '@heroui/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const colorMap = {
|
const colorMap = {
|
||||||
@ -16,9 +16,9 @@ const LogItem: React.FC<IMihomoLogInfo & { index: number }> = (props) => {
|
|||||||
<div className={`mr-2 text-lg font-bold text-${colorMap[type]}`}>
|
<div className={`mr-2 text-lg font-bold text-${colorMap[type]}`}>
|
||||||
{props.type.toUpperCase()}
|
{props.type.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<small className="text-default-500">{time}</small>
|
<small className="text-foreground-500">{time}</small>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="pt-0 text-sm">{payload}</CardBody>
|
<CardBody className="select-text pt-0 text-sm">{payload}</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,14 +6,18 @@ import {
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
Button,
|
Button,
|
||||||
Snippet
|
Snippet
|
||||||
} from '@nextui-org/react'
|
} from '@heroui/react'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { getInterfaces } from '@renderer/utils/ipc'
|
import { getInterfaces } from '@renderer/utils/ipc'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const InterfaceModal: React.FC<Props> = (props) => {
|
const InterfaceModal: React.FC<Props> = (props) => {
|
||||||
const { onClose } = props
|
const { onClose } = props
|
||||||
|
const { t } = useTranslation()
|
||||||
const [info, setInfo] = useState<Record<string, NetworkInterfaceInfo[]>>({})
|
const [info, setInfo] = useState<Record<string, NetworkInterfaceInfo[]>>({})
|
||||||
const getInfo = async (): Promise<void> => {
|
const getInfo = async (): Promise<void> => {
|
||||||
setInfo(await getInterfaces())
|
setInfo(await getInterfaces())
|
||||||
@ -33,7 +37,7 @@ const InterfaceModal: React.FC<Props> = (props) => {
|
|||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader className="flex">网络信息</ModalHeader>
|
<ModalHeader className="flex app-drag">{t('mihomo.interface.title')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{Object.entries(info).map(([key, value]) => {
|
{Object.entries(info).map(([key, value]) => {
|
||||||
return (
|
return (
|
||||||
@ -56,8 +60,8 @@ const InterfaceModal: React.FC<Props> = (props) => {
|
|||||||
})}
|
})}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="light" onPress={onClose}>
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
关闭
|
{t('common.close')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { BaseEditor } from '../base/base-editor'
|
import { BaseEditor } from '../base/base-editor'
|
||||||
import { getOverride, restartCore, setOverride } from '@renderer/utils/ipc'
|
import { getOverride, restartCore, setOverride } from '@renderer/utils/ipc'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string
|
id: string
|
||||||
language: 'javascript' | 'yaml'
|
language: 'javascript' | 'yaml'
|
||||||
@ -10,6 +12,7 @@ interface Props {
|
|||||||
const EditFileModal: React.FC<Props> = (props) => {
|
const EditFileModal: React.FC<Props> = (props) => {
|
||||||
const { id, language, onClose } = props
|
const { id, language, onClose } = props
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const getContent = async (): Promise<void> => {
|
const getContent = async (): Promise<void> => {
|
||||||
setCurrData(await getOverride(id, language === 'javascript' ? 'js' : 'yaml'))
|
setCurrData(await getOverride(id, language === 'javascript' ? 'js' : 'yaml'))
|
||||||
@ -30,8 +33,10 @@ const EditFileModal: React.FC<Props> = (props) => {
|
|||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
>
|
>
|
||||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||||
<ModalHeader className="flex pb-0">
|
<ModalHeader className="flex pb-0 app-drag">
|
||||||
编辑覆写{language === 'javascript' ? '脚本' : '配置'}
|
{t('override.editFile.title', {
|
||||||
|
type: language === 'javascript' ? t('override.editFile.script') : t('override.editFile.config')
|
||||||
|
})}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody className="h-full">
|
<ModalBody className="h-full">
|
||||||
<BaseEditor
|
<BaseEditor
|
||||||
@ -42,7 +47,7 @@ const EditFileModal: React.FC<Props> = (props) => {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter className="pt-0">
|
<ModalFooter className="pt-0">
|
||||||
<Button size="sm" variant="light" onPress={onClose}>
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
取消
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -57,7 +62,7 @@ const EditFileModal: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
Switch
|
Switch
|
||||||
} from '@nextui-org/react'
|
} from '@heroui/react'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { restartCore } from '@renderer/utils/ipc'
|
import { restartCore } from '@renderer/utils/ipc'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: IOverrideItem
|
item: IOverrideItem
|
||||||
updateOverrideItem: (item: IOverrideItem) => Promise<void>
|
updateOverrideItem: (item: IOverrideItem) => Promise<void>
|
||||||
@ -19,6 +21,7 @@ interface Props {
|
|||||||
const EditInfoModal: React.FC<Props> = (props) => {
|
const EditInfoModal: React.FC<Props> = (props) => {
|
||||||
const { item, updateOverrideItem, onClose } = props
|
const { item, updateOverrideItem, onClose } = props
|
||||||
const [values, setValues] = useState(item)
|
const [values, setValues] = useState(item)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const onSave = async (): Promise<void> => {
|
const onSave = async (): Promise<void> => {
|
||||||
await updateOverrideItem(values)
|
await updateOverrideItem(values)
|
||||||
@ -36,9 +39,9 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader className="flex">编辑信息</ModalHeader>
|
<ModalHeader className="flex app-drag">{t('override.editInfo.title')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<SettingItem title="名称">
|
<SettingItem title={t('override.editInfo.name')}>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[200px]"
|
className="w-[200px]"
|
||||||
@ -49,7 +52,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
{values.type === 'remote' && (
|
{values.type === 'remote' && (
|
||||||
<SettingItem title="地址">
|
<SettingItem title={t('override.editInfo.url')}>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[200px]"
|
className="w-[200px]"
|
||||||
@ -60,7 +63,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
<SettingItem title="全局启用">
|
<SettingItem title={t('override.editInfo.global')}>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={values.global}
|
isSelected={values.global}
|
||||||
@ -71,11 +74,11 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="light" onPress={onClose}>
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
取消
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" onPress={onSave}>
|
<Button size="sm" color="primary" onPress={onSave}>
|
||||||
保存
|
{t('common.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import {
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
Button,
|
Button,
|
||||||
Divider
|
Divider
|
||||||
} from '@nextui-org/react'
|
} from '@heroui/react'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { getOverride } from '@renderer/utils/ipc'
|
import { getOverride } from '@renderer/utils/ipc'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string
|
id: string
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@ -16,6 +18,7 @@ interface Props {
|
|||||||
const ExecLogModal: React.FC<Props> = (props) => {
|
const ExecLogModal: React.FC<Props> = (props) => {
|
||||||
const { id, onClose } = props
|
const { id, onClose } = props
|
||||||
const [logs, setLogs] = useState<string[]>([])
|
const [logs, setLogs] = useState<string[]>([])
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const getLog = async (): Promise<void> => {
|
const getLog = async (): Promise<void> => {
|
||||||
setLogs((await getOverride(id, 'log')).split('\n').filter(Boolean))
|
setLogs((await getOverride(id, 'log')).split('\n').filter(Boolean))
|
||||||
@ -35,7 +38,7 @@ const ExecLogModal: React.FC<Props> = (props) => {
|
|||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader className="flex">执行日志</ModalHeader>
|
<ModalHeader className="flex app-drag">{t('override.execLog.title')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{logs.map((log) => {
|
{logs.map((log) => {
|
||||||
return (
|
return (
|
||||||
@ -47,8 +50,8 @@ const ExecLogModal: React.FC<Props> = (props) => {
|
|||||||
})}
|
})}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="light" onPress={onClose}>
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
关闭
|
{t('override.execLog.close')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import {
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownTrigger
|
DropdownTrigger
|
||||||
} from '@nextui-org/react'
|
} from '@heroui/react'
|
||||||
import { IoMdMore, IoMdRefresh } from 'react-icons/io'
|
import { IoMdMore, IoMdRefresh } from 'react-icons/io'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from '@renderer/utils/dayjs'
|
||||||
import React, { Key, useEffect, useMemo, useState } from 'react'
|
import React, { Key, useEffect, useMemo, useState } from 'react'
|
||||||
import EditFileModal from './edit-file-modal'
|
import EditFileModal from './edit-file-modal'
|
||||||
import EditInfoModal from './edit-info-modal'
|
import EditInfoModal from './edit-info-modal'
|
||||||
@ -17,6 +17,7 @@ import { useSortable } from '@dnd-kit/sortable'
|
|||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import ExecLogModal from './exec-log-modal'
|
import ExecLogModal from './exec-log-modal'
|
||||||
import { openFile, restartCore } from '@renderer/utils/ipc'
|
import { openFile, restartCore } from '@renderer/utils/ipc'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
info: IOverrideItem
|
info: IOverrideItem
|
||||||
@ -35,6 +36,7 @@ interface MenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OverrideItem: React.FC<Props> = (props) => {
|
const OverrideItem: React.FC<Props> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { info, addOverrideItem, removeOverrideItem, mutateOverrideConfig, updateOverrideItem } =
|
const { info, addOverrideItem, removeOverrideItem, mutateOverrideConfig, updateOverrideItem } =
|
||||||
props
|
props
|
||||||
const [updating, setUpdating] = useState(false)
|
const [updating, setUpdating] = useState(false)
|
||||||
@ -57,35 +59,35 @@ const OverrideItem: React.FC<Props> = (props) => {
|
|||||||
const list = [
|
const list = [
|
||||||
{
|
{
|
||||||
key: 'edit-info',
|
key: 'edit-info',
|
||||||
label: '编辑信息',
|
label: t('override.menuItems.editInfo'),
|
||||||
showDivider: false,
|
showDivider: false,
|
||||||
color: 'default',
|
color: 'default',
|
||||||
className: ''
|
className: ''
|
||||||
} as MenuItem,
|
} as MenuItem,
|
||||||
{
|
{
|
||||||
key: 'edit-file',
|
key: 'edit-file',
|
||||||
label: '编辑文件',
|
label: t('override.menuItems.editFile'),
|
||||||
showDivider: false,
|
showDivider: false,
|
||||||
color: 'default',
|
color: 'default',
|
||||||
className: ''
|
className: ''
|
||||||
} as MenuItem,
|
} as MenuItem,
|
||||||
{
|
{
|
||||||
key: 'open-file',
|
key: 'open-file',
|
||||||
label: '打开文件',
|
label: t('override.menuItems.openFile'),
|
||||||
showDivider: false,
|
showDivider: false,
|
||||||
color: 'default',
|
color: 'default',
|
||||||
className: ''
|
className: ''
|
||||||
} as MenuItem,
|
} as MenuItem,
|
||||||
{
|
{
|
||||||
key: 'exec-log',
|
key: 'exec-log',
|
||||||
label: '执行日志',
|
label: t('override.menuItems.execLog'),
|
||||||
showDivider: true,
|
showDivider: true,
|
||||||
color: 'default',
|
color: 'default',
|
||||||
className: ''
|
className: ''
|
||||||
} as MenuItem,
|
} as MenuItem,
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: '删除',
|
label: t('override.menuItems.delete'),
|
||||||
showDivider: false,
|
showDivider: false,
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
className: 'text-danger'
|
className: 'text-danger'
|
||||||
@ -95,7 +97,7 @@ const OverrideItem: React.FC<Props> = (props) => {
|
|||||||
list.splice(3, 1)
|
list.splice(3, 1)
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}, [info])
|
}, [info, t])
|
||||||
const onMenuAction = (key: Key): void => {
|
const onMenuAction = (key: Key): void => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'edit-info': {
|
case 'edit-info': {
|
||||||
@ -160,6 +162,7 @@ const OverrideItem: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
{openLog && <ExecLogModal id={info.id} onClose={() => setOpenLog(false)} />}
|
{openLog && <ExecLogModal id={info.id} onClose={() => setOpenLog(false)} />}
|
||||||
<Card
|
<Card
|
||||||
|
as="div"
|
||||||
fullWidth
|
fullWidth
|
||||||
isPressable
|
isPressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@ -167,12 +170,10 @@ const OverrideItem: React.FC<Props> = (props) => {
|
|||||||
setOpenFileEditor(true)
|
setOpenFileEditor(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div ref={setNodeRef} {...attributes} {...listeners} className="h-full w-full">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<div className="flex justify-between h-[32px]">
|
<div className="flex justify-between h-[32px]">
|
||||||
<h3
|
<h3
|
||||||
ref={setNodeRef}
|
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
title={info?.name}
|
title={info?.name}
|
||||||
className={`text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px] text-foreground`}
|
className={`text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px] text-foreground`}
|
||||||
>
|
>
|
||||||
@ -230,12 +231,9 @@ const OverrideItem: React.FC<Props> = (props) => {
|
|||||||
<div className={`mt-2 flex justify-start`}>
|
<div className={`mt-2 flex justify-start`}>
|
||||||
{info.global && (
|
{info.global && (
|
||||||
<Chip size="sm" variant="dot" color="primary" className="mr-2">
|
<Chip size="sm" variant="dot" color="primary" className="mr-2">
|
||||||
全局
|
{t('override.labels.global')}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
<Chip size="sm" variant="bordered" className="mr-2">
|
|
||||||
{info.type === 'local' ? '本地' : '远程'}
|
|
||||||
</Chip>
|
|
||||||
<Chip size="sm" variant="bordered">
|
<Chip size="sm" variant="bordered">
|
||||||
{info.ext === 'yaml' ? 'YAML' : 'JavaScript'}
|
{info.ext === 'yaml' ? 'YAML' : 'JavaScript'}
|
||||||
</Chip>
|
</Chip>
|
||||||
@ -247,6 +245,7 @@ const OverrideItem: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { BaseEditor } from '../base/base-editor'
|
import { BaseEditor } from '../base/base-editor'
|
||||||
import { getProfileStr, setProfileStr } from '@renderer/utils/ipc'
|
import { getProfileStr, setProfileStr } from '@renderer/utils/ipc'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string
|
id: string
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditFileModal: React.FC<Props> = (props) => {
|
const EditFileModal: React.FC<Props> = (props) => {
|
||||||
const { id, onClose } = props
|
const { id, onClose } = props
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const getContent = async (): Promise<void> => {
|
const getContent = async (): Promise<void> => {
|
||||||
setCurrData(await getProfileStr(id))
|
setCurrData(await getProfileStr(id))
|
||||||
@ -31,22 +35,23 @@ const EditFileModal: React.FC<Props> = (props) => {
|
|||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
>
|
>
|
||||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||||
<ModalHeader className="flex pb-0">
|
<ModalHeader className="flex pb-0 app-drag">
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="flex items-center">编辑订阅</div>
|
<div className="flex items-center">{t('profiles.editFile.title')}</div>
|
||||||
<small className="ml-2 text-default-500">
|
<small className="ml-2 text-foreground-500">
|
||||||
注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用
|
{t('profiles.editFile.notice')}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="light"
|
variant="light"
|
||||||
|
className="app-nodrag"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigate('/override')
|
navigate('/override')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
覆写
|
{t('profiles.editFile.override')}
|
||||||
</Button>
|
</Button>
|
||||||
功能
|
{t('profiles.editFile.feature')}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
@ -55,7 +60,7 @@ const EditFileModal: React.FC<Props> = (props) => {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter className="pt-0">
|
<ModalFooter className="pt-0">
|
||||||
<Button size="sm" variant="light" onPress={onClose}>
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
取消
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -65,7 +70,7 @@ const EditFileModal: React.FC<Props> = (props) => {
|
|||||||
onClose()
|
onClose()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
cn,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
@ -11,13 +12,15 @@ import {
|
|||||||
DropdownTrigger,
|
DropdownTrigger,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem
|
DropdownItem
|
||||||
} from '@nextui-org/react'
|
} from '@heroui/react'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { useOverrideConfig } from '@renderer/hooks/use-override-config'
|
import { useOverrideConfig } from '@renderer/hooks/use-override-config'
|
||||||
import { restartCore } from '@renderer/utils/ipc'
|
import { restartCore, addProfileUpdater } from '@renderer/utils/ipc'
|
||||||
import { MdDeleteForever } from 'react-icons/md'
|
import { MdDeleteForever } from 'react-icons/md'
|
||||||
import { FaPlus } from 'react-icons/fa6'
|
import { FaPlus } from 'react-icons/fa6'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: IProfileItem
|
item: IProfileItem
|
||||||
updateProfileItem: (item: IProfileItem) => Promise<void>
|
updateProfileItem: (item: IProfileItem) => Promise<void>
|
||||||
@ -28,16 +31,20 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
const { overrideConfig } = useOverrideConfig()
|
const { overrideConfig } = useOverrideConfig()
|
||||||
const { items: overrideItems = [] } = overrideConfig || {}
|
const { items: overrideItems = [] } = overrideConfig || {}
|
||||||
const [values, setValues] = useState(item)
|
const [values, setValues] = useState(item)
|
||||||
|
const inputWidth = 'w-[400px] md:w-[400px] lg:w-[600px] xl:w-[800px]'
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const onSave = async (): Promise<void> => {
|
const onSave = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await updateProfileItem({
|
const updatedItem = {
|
||||||
...values,
|
...values,
|
||||||
override: values.override?.filter(
|
override: values.override?.filter(
|
||||||
(i) =>
|
(i) =>
|
||||||
overrideItems.find((t) => t.id === i) && !overrideItems.find((t) => t.id === i)?.global
|
overrideItems.find((t) => t.id === i) && !overrideItems.find((t) => t.id === i)?.global
|
||||||
)
|
)
|
||||||
})
|
};
|
||||||
|
await updateProfileItem(updatedItem)
|
||||||
|
await addProfileUpdater(updatedItem)
|
||||||
await restartCore()
|
await restartCore()
|
||||||
onClose()
|
onClose()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -48,19 +55,23 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
classNames={{ backdrop: 'top-[48px]' }}
|
size="5xl"
|
||||||
|
classNames={{
|
||||||
|
backdrop: 'top-[48px]',
|
||||||
|
base: 'w-[600px] md:w-[600px] lg:w-[800px] xl:w-[1024px]'
|
||||||
|
}}
|
||||||
hideCloseButton
|
hideCloseButton
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
onOpenChange={onClose}
|
onOpenChange={onClose}
|
||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader className="flex">编辑信息</ModalHeader>
|
<ModalHeader className="flex app-drag">{t('profiles.editInfo.title')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<SettingItem title="名称">
|
<SettingItem title={t('profiles.editInfo.name')}>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[200px]"
|
className={cn(inputWidth)}
|
||||||
value={values.name}
|
value={values.name}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setValues({ ...values, name: v })
|
setValues({ ...values, name: v })
|
||||||
@ -69,17 +80,17 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
{values.type === 'remote' && (
|
{values.type === 'remote' && (
|
||||||
<>
|
<>
|
||||||
<SettingItem title="订阅地址">
|
<SettingItem title={t('profiles.editInfo.url')}>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[200px]"
|
className={cn(inputWidth)}
|
||||||
value={values.url}
|
value={values.url}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setValues({ ...values, url: v })
|
setValues({ ...values, url: v })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="使用代理更新">
|
<SettingItem title={t('profiles.editInfo.useProxy')}>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={values.useProxy ?? false}
|
isSelected={values.useProxy ?? false}
|
||||||
@ -88,20 +99,29 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="更新间隔(分钟)">
|
<SettingItem title={t('profiles.editInfo.interval')}>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
type="number"
|
type="number"
|
||||||
className="w-[200px]"
|
className={cn(inputWidth)}
|
||||||
value={values.interval?.toString() ?? ''}
|
value={values.interval?.toString() ?? ''}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setValues({ ...values, interval: parseInt(v) })
|
setValues({ ...values, interval: parseInt(v) })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem title={t('profiles.editInfo.fixedInterval')}>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={values.allowFixedInterval ?? false}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setValues({ ...values, allowFixedInterval: v })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<SettingItem title="覆写">
|
<SettingItem title={t('profiles.editInfo.override.title')}>
|
||||||
<div>
|
<div>
|
||||||
{overrideItems
|
{overrideItems
|
||||||
.filter((i) => i.global)
|
.filter((i) => i.global)
|
||||||
@ -109,7 +129,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex mb-2" key={i.id}>
|
<div className="flex mb-2" key={i.id}>
|
||||||
<Button disabled fullWidth variant="flat" size="sm">
|
<Button disabled fullWidth variant="flat" size="sm">
|
||||||
{i.name} (全局)
|
{i.name} ({t('profiles.editInfo.override.global')})
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -146,7 +166,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
emptyContent="没有可用的覆写"
|
emptyContent={t('profiles.editInfo.override.noAvailable')}
|
||||||
onAction={(key) => {
|
onAction={(key) => {
|
||||||
setValues({
|
setValues({
|
||||||
...values,
|
...values,
|
||||||
@ -165,11 +185,11 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="light" onPress={onClose}>
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
取消
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" onPress={onSave}>
|
<Button size="sm" color="primary" onPress={onSave}>
|
||||||
保存
|
{t('common.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@ -8,17 +8,20 @@ import {
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownTrigger,
|
DropdownTrigger,
|
||||||
Progress
|
Progress,
|
||||||
} from '@nextui-org/react'
|
Tooltip
|
||||||
|
} from '@heroui/react'
|
||||||
import { calcPercent, calcTraffic } from '@renderer/utils/calc'
|
import { calcPercent, calcTraffic } from '@renderer/utils/calc'
|
||||||
import { IoMdMore, IoMdRefresh } from 'react-icons/io'
|
import { IoMdMore, IoMdRefresh } from 'react-icons/io'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from '@renderer/utils/dayjs'
|
||||||
import React, { Key, useEffect, useMemo, useState } from 'react'
|
import React, { Key, useEffect, useMemo, useState } from 'react'
|
||||||
import EditFileModal from './edit-file-modal'
|
import EditFileModal from './edit-file-modal'
|
||||||
import EditInfoModal from './edit-info-modal'
|
import EditInfoModal from './edit-info-modal'
|
||||||
import { useSortable } from '@dnd-kit/sortable'
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { openFile } from '@renderer/utils/ipc'
|
import { openFile } from '@renderer/utils/ipc'
|
||||||
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
info: IProfileItem
|
info: IProfileItem
|
||||||
@ -27,7 +30,7 @@ interface Props {
|
|||||||
updateProfileItem: (item: IProfileItem) => Promise<void>
|
updateProfileItem: (item: IProfileItem) => Promise<void>
|
||||||
removeProfileItem: (id: string) => Promise<void>
|
removeProfileItem: (id: string) => Promise<void>
|
||||||
mutateProfileConfig: () => void
|
mutateProfileConfig: () => void
|
||||||
onClick: () => Promise<void>
|
onPress: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
@ -38,22 +41,26 @@ interface MenuItem {
|
|||||||
className: string
|
className: string
|
||||||
}
|
}
|
||||||
const ProfileItem: React.FC<Props> = (props) => {
|
const ProfileItem: React.FC<Props> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
info,
|
info,
|
||||||
addProfileItem,
|
addProfileItem,
|
||||||
removeProfileItem,
|
removeProfileItem,
|
||||||
mutateProfileConfig,
|
mutateProfileConfig,
|
||||||
updateProfileItem,
|
updateProfileItem,
|
||||||
onClick,
|
onPress,
|
||||||
isCurrent
|
isCurrent
|
||||||
} = props
|
} = props
|
||||||
const extra = info?.extra
|
const extra = info?.extra
|
||||||
const usage = (extra?.upload ?? 0) + (extra?.download ?? 0)
|
const usage = (extra?.upload ?? 0) + (extra?.download ?? 0)
|
||||||
const total = extra?.total ?? 0
|
const total = extra?.total ?? 0
|
||||||
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
|
const { profileDisplayDate = 'expire' } = appConfig || {}
|
||||||
const [updating, setUpdating] = useState(false)
|
const [updating, setUpdating] = useState(false)
|
||||||
const [selecting, setSelecting] = useState(false)
|
const [selecting, setSelecting] = useState(false)
|
||||||
const [openInfoEditor, setOpenInfoEditor] = useState(false)
|
const [openInfoEditor, setOpenInfoEditor] = useState(false)
|
||||||
const [openFileEditor, setOpenFileEditor] = useState(false)
|
const [openFileEditor, setOpenFileEditor] = useState(false)
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
@ -71,28 +78,28 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
const list = [
|
const list = [
|
||||||
{
|
{
|
||||||
key: 'edit-info',
|
key: 'edit-info',
|
||||||
label: '编辑信息',
|
label: t('profiles.editInfo.title'),
|
||||||
showDivider: false,
|
showDivider: false,
|
||||||
color: 'default',
|
color: 'default',
|
||||||
className: ''
|
className: ''
|
||||||
} as MenuItem,
|
} as MenuItem,
|
||||||
{
|
{
|
||||||
key: 'edit-file',
|
key: 'edit-file',
|
||||||
label: '编辑文件',
|
label: t('profiles.editFile.title'),
|
||||||
showDivider: false,
|
showDivider: false,
|
||||||
color: 'default',
|
color: 'default',
|
||||||
className: ''
|
className: ''
|
||||||
} as MenuItem,
|
} as MenuItem,
|
||||||
{
|
{
|
||||||
key: 'open-file',
|
key: 'open-file',
|
||||||
label: '打开文件',
|
label: t('profiles.openFile'),
|
||||||
showDivider: true,
|
showDivider: true,
|
||||||
color: 'default',
|
color: 'default',
|
||||||
className: ''
|
className: ''
|
||||||
} as MenuItem,
|
} as MenuItem,
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: '删除',
|
label: t('common.delete'),
|
||||||
showDivider: false,
|
showDivider: false,
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
className: 'text-danger'
|
className: 'text-danger'
|
||||||
@ -101,14 +108,14 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
if (info.home) {
|
if (info.home) {
|
||||||
list.unshift({
|
list.unshift({
|
||||||
key: 'home',
|
key: 'home',
|
||||||
label: '主页',
|
label: t('profiles.home'),
|
||||||
showDivider: false,
|
showDivider: false,
|
||||||
color: 'default',
|
color: 'default',
|
||||||
className: ''
|
className: ''
|
||||||
} as MenuItem)
|
} as MenuItem)
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}, [info])
|
}, [info, t])
|
||||||
|
|
||||||
const onMenuAction = async (key: Key): Promise<void> => {
|
const onMenuAction = async (key: Key): Promise<void> => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
@ -137,6 +144,12 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleContextMenu = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setDropdownOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -149,6 +162,8 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [isDragging])
|
}, [isDragging])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="grid col-span-1"
|
className="grid col-span-1"
|
||||||
@ -167,37 +182,38 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
updateProfileItem={updateProfileItem}
|
updateProfileItem={updateProfileItem}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
|
as="div"
|
||||||
fullWidth
|
fullWidth
|
||||||
isPressable
|
isPressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (disableSelect) return
|
if (disableSelect) return
|
||||||
setSelecting(true)
|
setSelecting(true)
|
||||||
onClick().finally(() => {
|
onPress().finally(() => {
|
||||||
setSelecting(false)
|
setSelecting(false)
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''}`}
|
className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''}`}
|
||||||
>
|
>
|
||||||
|
<div ref={setNodeRef} {...attributes} {...listeners} className="w-full h-full">
|
||||||
<CardBody className="pb-1">
|
<CardBody className="pb-1">
|
||||||
<div className="flex justify-between h-[32px]">
|
<div className="flex justify-between h-[32px]">
|
||||||
<h3
|
<h3
|
||||||
ref={setNodeRef}
|
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
title={info?.name}
|
title={info?.name}
|
||||||
className={`text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px] ${isCurrent ? 'text-white' : 'text-foreground'}`}
|
className={`text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px] ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
>
|
>
|
||||||
{info?.name}
|
{info?.name}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{info.type === 'remote' && (
|
{info.type === 'remote' && (
|
||||||
|
<Tooltip placement="left" content={dayjs(info.updated).fromNow()}>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
color="default"
|
color="default"
|
||||||
title={dayjs(info.updated).fromNow()}
|
|
||||||
disabled={updating}
|
disabled={updating}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setUpdating(true)
|
setUpdating(true)
|
||||||
@ -207,17 +223,21 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
>
|
>
|
||||||
<IoMdRefresh
|
<IoMdRefresh
|
||||||
color="default"
|
color="default"
|
||||||
className={`${isCurrent ? 'text-white' : 'text-foreground'} text-[24px] ${updating ? 'animate-spin' : ''}`}
|
className={`${isCurrent ? 'text-primary-foreground' : 'text-foreground'} text-[24px] ${updating ? 'animate-spin' : ''}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dropdown>
|
<Dropdown
|
||||||
|
isOpen={dropdownOpen}
|
||||||
|
onOpenChange={setDropdownOpen}
|
||||||
|
>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button isIconOnly size="sm" variant="light" color="default">
|
<Button isIconOnly size="sm" variant="light" color="default">
|
||||||
<IoMdMore
|
<IoMdMore
|
||||||
color="default"
|
color="default"
|
||||||
className={`text-[24px] ${isCurrent ? 'text-white' : 'text-foreground'}`}
|
className={`text-[24px] ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
@ -238,39 +258,75 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
{info.type === 'remote' && extra && (
|
{info.type === 'remote' && extra && (
|
||||||
<div
|
<div
|
||||||
className={`mt-2 flex justify-between ${isCurrent ? 'text-white' : 'text-foreground'}`}
|
className={`mt-2 flex justify-between ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
>
|
>
|
||||||
<small>{`${calcTraffic(usage)}/${calcTraffic(total)}`}</small>
|
<small>{`${calcTraffic(usage)}/${calcTraffic(total)}`}</small>
|
||||||
<small>
|
{profileDisplayDate === 'expire' ? (
|
||||||
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : '长期有效'}
|
<Button
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{info.type === 'local' && (
|
|
||||||
<div
|
|
||||||
className={`mt-2 flex justify-between ${isCurrent ? 'text-white' : 'text-foreground'}`}
|
|
||||||
>
|
|
||||||
<Chip
|
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="bordered"
|
variant="light"
|
||||||
className={`${isCurrent ? 'text-white border-white' : 'border-primary text-primary'}`}
|
className={`h-[20px] p-1 m-0 ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
|
onPress={async () => {
|
||||||
|
await patchAppConfig({ profileDisplayDate: 'update' })
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
本地
|
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : t('profiles.neverExpire')}
|
||||||
</Chip>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
className={`h-[20px] p-1 m-0 ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
|
onPress={async () => {
|
||||||
|
await patchAppConfig({ profileDisplayDate: 'expire' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dayjs(info.updated).fromNow()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="pt-0">
|
<CardFooter className="pt-0">
|
||||||
|
{info.type === 'remote' && !extra && (
|
||||||
|
<div
|
||||||
|
className={`w-full mt-2 flex justify-between ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
variant="bordered"
|
||||||
|
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
|
||||||
|
>
|
||||||
|
{t('profiles.remote')}
|
||||||
|
</Chip>
|
||||||
|
<small>{dayjs(info.updated).fromNow()}</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{info.type === 'local' && (
|
||||||
|
<div
|
||||||
|
className={`mt-2 flex justify-between ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
variant="bordered"
|
||||||
|
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
|
||||||
|
>
|
||||||
|
{t('profiles.local')}
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{extra && (
|
{extra && (
|
||||||
<Progress
|
<Progress
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
aria-label={t('profiles.trafficUsage')}
|
||||||
classNames={{
|
classNames={{
|
||||||
indicator: isCurrent ? 'bg-white' : 'bg-foreground'
|
indicator: isCurrent ? 'bg-primary-foreground' : 'bg-foreground'
|
||||||
}}
|
}}
|
||||||
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
|
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { Button, Card, CardBody } from '@nextui-org/react'
|
import { Button, Card, CardBody } from '@heroui/react'
|
||||||
import React, { useMemo, useState } from 'react'
|
import { mihomoUnfixedProxy } from '@renderer/utils/ipc'
|
||||||
|
import React from 'react'
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { FaMapPin } from 'react-icons/fa6'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mutateProxies: () => void
|
mutateProxies: () => void
|
||||||
@ -9,10 +13,12 @@ interface Props {
|
|||||||
group: IMihomoMixedGroup
|
group: IMihomoMixedGroup
|
||||||
onSelect: (group: string, proxy: string) => void
|
onSelect: (group: string, proxy: string) => void
|
||||||
selected: boolean
|
selected: boolean
|
||||||
|
isGroupTesting?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProxyItem: React.FC<Props> = (props) => {
|
const ProxyItem: React.FC<Props> = (props) => {
|
||||||
const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay } = props
|
const { t } = useTranslation()
|
||||||
|
const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay, isGroupTesting = false } = props
|
||||||
|
|
||||||
const delay = useMemo(() => {
|
const delay = useMemo(() => {
|
||||||
if (proxy.history.length > 0) {
|
if (proxy.history.length > 0) {
|
||||||
@ -23,6 +29,8 @@ const ProxyItem: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const isLoading = loading || isGroupTesting
|
||||||
|
|
||||||
function delayColor(delay: number): 'primary' | 'success' | 'warning' | 'danger' {
|
function delayColor(delay: number): 'primary' | 'success' | 'warning' | 'danger' {
|
||||||
if (delay === -1) return 'primary'
|
if (delay === -1) return 'primary'
|
||||||
if (delay === 0) return 'danger'
|
if (delay === 0) return 'danger'
|
||||||
@ -31,8 +39,8 @@ const ProxyItem: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function delayText(delay: number): string {
|
function delayText(delay: number): string {
|
||||||
if (delay === -1) return '测试'
|
if (delay === -1) return t('proxies.delay.test')
|
||||||
if (delay === 0) return '超时'
|
if (delay === 0) return t('proxies.delay.timeout')
|
||||||
return delay.toString()
|
return delay.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,36 +56,112 @@ const ProxyItem: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
as="div"
|
||||||
onPress={() => onSelect(group.name, proxy.name)}
|
onPress={() => onSelect(group.name, proxy.name)}
|
||||||
isPressable
|
isPressable
|
||||||
fullWidth
|
fullWidth
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
className={`${fixed ? 'bg-secondary/30' : selected ? 'bg-primary/30' : 'bg-content2'}`}
|
className={`${
|
||||||
|
fixed
|
||||||
|
? 'bg-secondary/30 border-r-2 border-r-secondary border-l-2 border-l-secondary'
|
||||||
|
: selected
|
||||||
|
? 'bg-primary/30 border-r-2 border-r-primary border-l-2 border-l-primary'
|
||||||
|
: 'bg-content2'
|
||||||
|
}`}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
>
|
>
|
||||||
<CardBody className="p-2">
|
<CardBody className="p-1">
|
||||||
<div className="flex justify-between items-center">
|
{proxyDisplayMode === 'full' ? (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex justify-between items-center pl-1">
|
||||||
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
<div className="flag-emoji inline" title={proxy.name}>
|
<div className="flag-emoji inline" title={proxy.name}>
|
||||||
{proxy.name}
|
{proxy.name}
|
||||||
</div>
|
</div>
|
||||||
{proxyDisplayMode === 'full' && (
|
</div>
|
||||||
<div className="inline ml-2 text-default-500" title={proxy.type}>
|
{fixed && (
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
title={t('proxies.unpin')}
|
||||||
|
color="danger"
|
||||||
|
onPress={async () => {
|
||||||
|
await mihomoUnfixedProxy(group.name)
|
||||||
|
mutateProxies()
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
className="h-[20px] p-0 text-sm"
|
||||||
|
>
|
||||||
|
<FaMapPin className="text-md le" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pl-1">
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
<div className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
|
||||||
{proxy.type}
|
{proxy.type}
|
||||||
</div>
|
</div>
|
||||||
|
{['tfo', 'udp', 'xudp', 'mptcp', 'smux'].map(protocol =>
|
||||||
|
proxy[protocol as keyof IMihomoProxy] && (
|
||||||
|
<div key={protocol} className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
|
||||||
|
{protocol}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
isIconOnly
|
||||||
title={proxy.type}
|
title={proxy.type}
|
||||||
isLoading={loading}
|
isLoading={isLoading}
|
||||||
color={delayColor(delay)}
|
color={delayColor(delay)}
|
||||||
onPress={onDelay}
|
onPress={onDelay}
|
||||||
variant="light"
|
variant="light"
|
||||||
className="h-full min-w-[50px] p-0 mx-2 text-sm hover:bg-content"
|
className="h-full text-sm ml-auto -mt-0.5 px-2 relative w-min whitespace-nowrap"
|
||||||
>
|
>
|
||||||
|
<div className="w-full h-full flex items-center justify-end">
|
||||||
{delayText(delay)}
|
{delayText(delay)}
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-between items-center pl-1">
|
||||||
|
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
|
<div className="flag-emoji inline" title={proxy.name}>
|
||||||
|
{proxy.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
{fixed && (
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
title={t('proxies.unpin')}
|
||||||
|
color="danger"
|
||||||
|
onPress={async () => {
|
||||||
|
await mihomoUnfixedProxy(group.name)
|
||||||
|
mutateProxies()
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
className="h-[20px] p-0 text-sm"
|
||||||
|
>
|
||||||
|
<FaMapPin className="text-md le" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
title={proxy.type}
|
||||||
|
isLoading={isLoading}
|
||||||
|
color={delayColor(delay)}
|
||||||
|
onPress={onDelay}
|
||||||
|
variant="light"
|
||||||
|
className="h-full text-sm px-2 relative w-min whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<div className="w-full h-full flex items-center justify-end">
|
||||||
|
{delayText(delay)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
import { Button, Input, Switch, Tab, Tabs } from '@nextui-org/react'
|
import { Button, Input, Switch, Tab, Tabs } from '@heroui/react'
|
||||||
import SettingCard from '@renderer/components/base/base-setting-card'
|
import SettingCard from '@renderer/components/base/base-setting-card'
|
||||||
import SettingItem from '@renderer/components/base/base-setting-item'
|
import SettingItem from '@renderer/components/base/base-setting-item'
|
||||||
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
|
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
|
||||||
import { mihomoUpgradeGeo } from '@renderer/utils/ipc'
|
import { mihomoUpgradeGeo } from '@renderer/utils/ipc'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { IoMdRefresh } from 'react-icons/io'
|
import { IoMdRefresh } from 'react-icons/io'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const GeoData: React.FC = () => {
|
const GeoData: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
|
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
|
||||||
const {
|
const {
|
||||||
'geox-url': geoxUrl = {
|
'geox-url': geoxUrl = {
|
||||||
geoip: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat',
|
geoip: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat',
|
||||||
geosite: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat',
|
geosite: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat',
|
||||||
mmdb: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb',
|
mmdb: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb',
|
||||||
asn: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb'
|
asn: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb'
|
||||||
},
|
},
|
||||||
'geodata-mode': geoMode = false,
|
'geodata-mode': geoMode = false,
|
||||||
@ -27,7 +29,7 @@ const GeoData: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingItem title="GeoIP 数据库" divider>
|
<SettingItem title={t('resources.geoData.geoip')} divider>
|
||||||
<div className="flex w-[70%]">
|
<div className="flex w-[70%]">
|
||||||
{geoipInput !== geoxUrl.geoip && (
|
{geoipInput !== geoxUrl.geoip && (
|
||||||
<Button
|
<Button
|
||||||
@ -38,13 +40,13 @@ const GeoData: React.FC = () => {
|
|||||||
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, geoip: geoipInput } })
|
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, geoip: geoipInput } })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Input size="sm" value={geoipInput} onValueChange={setGeoIpInput} />
|
<Input size="sm" value={geoipInput} onValueChange={setGeoIpInput} />
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="GeoSite 数据库" divider>
|
<SettingItem title={t('resources.geoData.geosite')} divider>
|
||||||
<div className="flex w-[70%]">
|
<div className="flex w-[70%]">
|
||||||
{geositeInput !== geoxUrl.geosite && (
|
{geositeInput !== geoxUrl.geosite && (
|
||||||
<Button
|
<Button
|
||||||
@ -55,13 +57,13 @@ const GeoData: React.FC = () => {
|
|||||||
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, geosite: geositeInput } })
|
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, geosite: geositeInput } })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Input size="sm" value={geositeInput} onValueChange={setGeositeInput} />
|
<Input size="sm" value={geositeInput} onValueChange={setGeositeInput} />
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="MMDB 数据库" divider>
|
<SettingItem title={t('resources.geoData.mmdb')} divider>
|
||||||
<div className="flex w-[70%]">
|
<div className="flex w-[70%]">
|
||||||
{mmdbInput !== geoxUrl.mmdb && (
|
{mmdbInput !== geoxUrl.mmdb && (
|
||||||
<Button
|
<Button
|
||||||
@ -72,13 +74,13 @@ const GeoData: React.FC = () => {
|
|||||||
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, mmdb: mmdbInput } })
|
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, mmdb: mmdbInput } })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Input size="sm" value={mmdbInput} onValueChange={setMmdbInput} />
|
<Input size="sm" value={mmdbInput} onValueChange={setMmdbInput} />
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="ASN 数据库" divider>
|
<SettingItem title={t('resources.geoData.asn')} divider>
|
||||||
<div className="flex w-[70%]">
|
<div className="flex w-[70%]">
|
||||||
{asnInput !== geoxUrl.asn && (
|
{asnInput !== geoxUrl.asn && (
|
||||||
<Button
|
<Button
|
||||||
@ -89,13 +91,13 @@ const GeoData: React.FC = () => {
|
|||||||
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, asn: asnInput } })
|
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, asn: asnInput } })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Input size="sm" value={asnInput} onValueChange={setAsnInput} />
|
<Input size="sm" value={asnInput} onValueChange={setAsnInput} />
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="GeoIP 数据模式" divider>
|
<SettingItem title={t('resources.geoData.mode')} divider>
|
||||||
<Tabs
|
<Tabs
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -109,7 +111,7 @@ const GeoData: React.FC = () => {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="自动更新 Geo 数据库"
|
title={t('resources.geoData.autoUpdate')}
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -119,7 +121,7 @@ const GeoData: React.FC = () => {
|
|||||||
setUpdating(true)
|
setUpdating(true)
|
||||||
try {
|
try {
|
||||||
await mihomoUpgradeGeo()
|
await mihomoUpgradeGeo()
|
||||||
new Notification('Geo 数据库更新成功')
|
new Notification(t('resources.geoData.updateSuccess'))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e)
|
alert(e)
|
||||||
} finally {
|
} finally {
|
||||||
@ -141,7 +143,7 @@ const GeoData: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
{geoAutoUpdate && (
|
{geoAutoUpdate && (
|
||||||
<SettingItem title="更新间隔(小时)">
|
<SettingItem title={t('resources.geoData.updateInterval')}>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@ -1,22 +1,59 @@
|
|||||||
import { mihomoProxyProviders, mihomoUpdateProxyProviders } from '@renderer/utils/ipc'
|
import {
|
||||||
import { Fragment, useMemo, useState } from 'react'
|
mihomoProxyProviders,
|
||||||
|
mihomoUpdateProxyProviders,
|
||||||
|
getRuntimeConfig
|
||||||
|
} from '@renderer/utils/ipc'
|
||||||
|
import { Fragment, useEffect, useMemo, useState } from 'react'
|
||||||
|
import Viewer from './viewer'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import SettingCard from '../base/base-setting-card'
|
import SettingCard from '../base/base-setting-card'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { Button, Chip } from '@nextui-org/react'
|
import { Button, Chip } from '@heroui/react'
|
||||||
import { IoMdRefresh } from 'react-icons/io'
|
import { IoMdRefresh } from 'react-icons/io'
|
||||||
import dayjs from 'dayjs'
|
import { CgLoadbarDoc } from 'react-icons/cg'
|
||||||
|
import { MdEditDocument } from 'react-icons/md'
|
||||||
|
import dayjs from '@renderer/utils/dayjs'
|
||||||
import { calcTraffic } from '@renderer/utils/calc'
|
import { calcTraffic } from '@renderer/utils/calc'
|
||||||
|
import { getHash } from '@renderer/utils/hash'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
const ProxyProvider: React.FC = () => {
|
const ProxyProvider: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [showDetails, setShowDetails] = useState({
|
||||||
|
show: false,
|
||||||
|
path: '',
|
||||||
|
type: '',
|
||||||
|
title: '',
|
||||||
|
privderType: ''
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
if (showDetails.title) {
|
||||||
|
const fetchProviderPath = async (name: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const providers= await getRuntimeConfig()
|
||||||
|
const provider = providers['proxy-providers'][name]
|
||||||
|
if (provider) {
|
||||||
|
setShowDetails((prev) => ({
|
||||||
|
...prev,
|
||||||
|
show: true,
|
||||||
|
path: provider?.path || `proxies/${getHash(provider?.url)}`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setShowDetails((prev) => ({ ...prev, path: '' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchProviderPath(showDetails.title)
|
||||||
|
}
|
||||||
|
}, [showDetails.title])
|
||||||
|
|
||||||
const { data, mutate } = useSWR('mihomoProxyProviders', mihomoProxyProviders)
|
const { data, mutate } = useSWR('mihomoProxyProviders', mihomoProxyProviders)
|
||||||
const providers = useMemo(() => {
|
const providers = useMemo(() => {
|
||||||
if (!data) return []
|
if (!data) return []
|
||||||
if (!data.providers) return []
|
return Object.values(data.providers)
|
||||||
return Object.keys(data.providers)
|
.filter((provider) => provider.vehicleType !== 'Compatible')
|
||||||
.map((key) => data.providers[key])
|
.sort((a, b) => {
|
||||||
.filter((provider) => {
|
const order = { File: 1, Inline: 2, HTTP: 3 }
|
||||||
return 'subscriptionInfo' in provider
|
return (order[a.vehicleType] || 4) - (order[b.vehicleType] || 4)
|
||||||
})
|
})
|
||||||
}, [data])
|
}, [data])
|
||||||
const [updating, setUpdating] = useState(Array(providers.length).fill(false))
|
const [updating, setUpdating] = useState(Array(providers.length).fill(false))
|
||||||
@ -45,7 +82,16 @@ const ProxyProvider: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingItem title="代理集合" divider>
|
{showDetails.show && (
|
||||||
|
<Viewer
|
||||||
|
path={showDetails.path}
|
||||||
|
type={showDetails.type}
|
||||||
|
title={showDetails.title}
|
||||||
|
privderType={showDetails.privderType}
|
||||||
|
onClose={() => setShowDetails({ show: false, path: '', type: '', title: '', privderType: '' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SettingItem title={t('resources.proxyProviders.title')} divider>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -55,11 +101,10 @@ const ProxyProvider: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
更新全部
|
{t('resources.proxyProviders.updateAll')}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
{providers.map((provider, index) => {
|
{providers.map((provider, index) => (
|
||||||
return (
|
|
||||||
<Fragment key={provider.name}>
|
<Fragment key={provider.name}>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title={provider.name}
|
title={provider.name}
|
||||||
@ -70,11 +115,35 @@ const ProxyProvider: React.FC = () => {
|
|||||||
}
|
}
|
||||||
divider={!provider.subscriptionInfo && index !== providers.length - 1}
|
divider={!provider.subscriptionInfo && index !== providers.length - 1}
|
||||||
>
|
>
|
||||||
{
|
<div className="flex h-[32px] leading-[32px] text-foreground-500">
|
||||||
<div className="flex h-[32px] leading-[32px] text-default-500">
|
|
||||||
<div>{dayjs(provider.updatedAt).fromNow()}</div>
|
<div>{dayjs(provider.updatedAt).fromNow()}</div>
|
||||||
|
{/* <Button isIconOnly className="ml-2" size="sm">
|
||||||
|
<IoMdEye className="text-lg" />
|
||||||
|
</Button> */}
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
title={provider.vehicleType == 'File' ? t('common.editor.edit') : t('common.viewer.view')}
|
||||||
|
className="ml-2"
|
||||||
|
size="sm"
|
||||||
|
onPress={() => {
|
||||||
|
setShowDetails({
|
||||||
|
show: false,
|
||||||
|
privderType: 'proxy-providers',
|
||||||
|
path: provider.name,
|
||||||
|
type: provider.vehicleType,
|
||||||
|
title: provider.name
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{provider.vehicleType == 'File' ? (
|
||||||
|
<MdEditDocument className={`text-lg`} />
|
||||||
|
) : (
|
||||||
|
<CgLoadbarDoc className={`text-lg`} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
title={t('common.updater.update')}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@ -84,30 +153,27 @@ const ProxyProvider: React.FC = () => {
|
|||||||
<IoMdRefresh className={`text-lg ${updating[index] ? 'animate-spin' : ''}`} />
|
<IoMdRefresh className={`text-lg ${updating[index] ? 'animate-spin' : ''}`} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
{provider.subscriptionInfo && (
|
{provider.subscriptionInfo && (
|
||||||
<SettingItem
|
<SettingItem
|
||||||
divider={index !== providers.length - 1}
|
divider={index !== providers.length - 1}
|
||||||
title={
|
title={
|
||||||
<div className="text-default-500">{`${calcTraffic(
|
<div className="text-foreground-500">
|
||||||
|
{`${calcTraffic(
|
||||||
provider.subscriptionInfo.Upload + provider.subscriptionInfo.Download
|
provider.subscriptionInfo.Upload + provider.subscriptionInfo.Download
|
||||||
)}
|
)} / ${calcTraffic(provider.subscriptionInfo.Total)}`}
|
||||||
/${calcTraffic(provider.subscriptionInfo.Total)}`}</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{provider.subscriptionInfo && (
|
<div className="h-[32px] leading-[32px] text-foreground-500">
|
||||||
<div className="h-[32px] leading-[32px] text-default-500">
|
|
||||||
{provider.subscriptionInfo.Expire
|
{provider.subscriptionInfo.Expire
|
||||||
? dayjs.unix(provider.subscriptionInfo.Expire).format('YYYY-MM-DD')
|
? dayjs.unix(provider.subscriptionInfo.Expire).format('YYYY-MM-DD')
|
||||||
: '长期有效'}
|
: t('profiles.neverExpire')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,64 @@
|
|||||||
import { mihomoRuleProviders, mihomoUpdateRuleProviders } from '@renderer/utils/ipc'
|
import {
|
||||||
import { Fragment, useMemo, useState } from 'react'
|
mihomoRuleProviders,
|
||||||
|
mihomoUpdateRuleProviders,
|
||||||
|
getRuntimeConfig
|
||||||
|
} from '@renderer/utils/ipc'
|
||||||
|
import { getHash } from '@renderer/utils/hash'
|
||||||
|
import Viewer from './viewer'
|
||||||
|
import { Fragment, useEffect, useMemo, useState } from 'react'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import SettingCard from '../base/base-setting-card'
|
import SettingCard from '../base/base-setting-card'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { Button, Chip } from '@nextui-org/react'
|
import { Button, Chip } from '@heroui/react'
|
||||||
import { IoMdRefresh } from 'react-icons/io'
|
import { IoMdRefresh } from 'react-icons/io'
|
||||||
import dayjs from 'dayjs'
|
import { CgLoadbarDoc } from 'react-icons/cg'
|
||||||
|
import { MdEditDocument } from 'react-icons/md'
|
||||||
|
import dayjs from '@renderer/utils/dayjs'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const RuleProvider: React.FC = () => {
|
const RuleProvider: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [showDetails, setShowDetails] = useState({
|
||||||
|
show: false,
|
||||||
|
path: '',
|
||||||
|
type: '',
|
||||||
|
title: '',
|
||||||
|
format: '',
|
||||||
|
privderType: ''
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
if (showDetails.title) {
|
||||||
|
const fetchProviderPath = async (name: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const providers= await getRuntimeConfig()
|
||||||
|
const provider = providers['rule-providers'][name]
|
||||||
|
if (provider) {
|
||||||
|
setShowDetails((prev) => ({
|
||||||
|
...prev,
|
||||||
|
show: true,
|
||||||
|
path: provider?.path || `rules/${getHash(provider?.url)}`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setShowDetails((prev) => ({ ...prev, path: '' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchProviderPath(showDetails.title)
|
||||||
|
}
|
||||||
|
}, [showDetails.title])
|
||||||
|
|
||||||
const { data, mutate } = useSWR('mihomoRuleProviders', mihomoRuleProviders)
|
const { data, mutate } = useSWR('mihomoRuleProviders', mihomoRuleProviders)
|
||||||
const providers = useMemo(() => {
|
const providers = useMemo(() => {
|
||||||
if (!data) return []
|
if (!data) return []
|
||||||
if (!data.providers) return []
|
return Object.values(data.providers).sort((a, b) => {
|
||||||
return Object.keys(data.providers).map((key) => data.providers[key])
|
if (a.vehicleType === 'File' && b.vehicleType !== 'File') {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (a.vehicleType !== 'File' && b.vehicleType === 'File') {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
}, [data])
|
}, [data])
|
||||||
const [updating, setUpdating] = useState(Array(providers.length).fill(false))
|
const [updating, setUpdating] = useState(Array(providers.length).fill(false))
|
||||||
|
|
||||||
@ -40,7 +86,17 @@ const RuleProvider: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingItem title="规则集合" divider>
|
{showDetails.show && (
|
||||||
|
<Viewer
|
||||||
|
path={showDetails.path}
|
||||||
|
type={showDetails.type}
|
||||||
|
title={showDetails.title}
|
||||||
|
format={showDetails.format}
|
||||||
|
privderType={showDetails.privderType}
|
||||||
|
onClose={() => setShowDetails({ show: false, path: '', type: '', title: '', format: '', privderType: '' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SettingItem title={t('resources.ruleProviders.title')} divider>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -50,11 +106,10 @@ const RuleProvider: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
更新全部
|
{t('resources.ruleProviders.updateAll')}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
{providers.map((provider, index) => {
|
{providers.map((provider, index) => (
|
||||||
return (
|
|
||||||
<Fragment key={provider.name}>
|
<Fragment key={provider.name}>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title={provider.name}
|
title={provider.name}
|
||||||
@ -64,11 +119,35 @@ const RuleProvider: React.FC = () => {
|
|||||||
</Chip>
|
</Chip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{
|
<div className="flex h-[32px] leading-[32px] text-foreground-500">
|
||||||
<div className="flex h-[32px] leading-[32px] text-default-500">
|
|
||||||
<div>{dayjs(provider.updatedAt).fromNow()}</div>
|
<div>{dayjs(provider.updatedAt).fromNow()}</div>
|
||||||
|
{provider.format !== 'MrsRule' && (
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
title={provider.vehicleType == 'File' ? t('common.editor.edit') : t('common.viewer.view')}
|
||||||
|
className="ml-2"
|
||||||
|
size="sm"
|
||||||
|
onPress={() => {
|
||||||
|
setShowDetails({
|
||||||
|
show: false,
|
||||||
|
privderType: 'rule-providers',
|
||||||
|
path: provider.name,
|
||||||
|
type: provider.vehicleType,
|
||||||
|
title: provider.name,
|
||||||
|
format: provider.format
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{provider.vehicleType == 'File' ? (
|
||||||
|
<MdEditDocument className={`text-lg`} />
|
||||||
|
) : (
|
||||||
|
<CgLoadbarDoc className={`text-lg`} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
title={t('common.updater.update')}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@ -78,19 +157,17 @@ const RuleProvider: React.FC = () => {
|
|||||||
<IoMdRefresh className={`text-lg ${updating[index] ? 'animate-spin' : ''}`} />
|
<IoMdRefresh className={`text-lg ${updating[index] ? 'animate-spin' : ''}`} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title={<div className="text-default-500">{provider.format}</div>}
|
title={<div className="text-foreground-500">{provider.format}</div>}
|
||||||
divider={index !== providers.length - 1}
|
divider={index !== providers.length - 1}
|
||||||
>
|
>
|
||||||
<div className="h-[32px] leading-[32px] text-default-500">
|
<div className="h-[32px] leading-[32px] text-foreground-500">
|
||||||
{provider.vehicleType}::{provider.behavior}
|
{provider.vehicleType}::{provider.behavior}
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
93
src/renderer/src/components/resources/viewer.tsx
Normal file
93
src/renderer/src/components/resources/viewer.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { BaseEditor } from '../base/base-editor'
|
||||||
|
import { getFileStr, setFileStr } from '@renderer/utils/ipc'
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void
|
||||||
|
path: string
|
||||||
|
type: string
|
||||||
|
title: string
|
||||||
|
privderType: string
|
||||||
|
format?: string
|
||||||
|
}
|
||||||
|
const Viewer: React.FC<Props> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { type, path, title, format, privderType, onClose } = props
|
||||||
|
const [currData, setCurrData] = useState('')
|
||||||
|
let language: Language = !format || format === 'YamlRule' ? 'yaml' : 'text'
|
||||||
|
|
||||||
|
const getContent = async (): Promise<void> => {
|
||||||
|
let fileContent: React.SetStateAction<string>
|
||||||
|
if (type === 'Inline') {
|
||||||
|
fileContent = await getFileStr('config.yaml')
|
||||||
|
language = 'yaml'
|
||||||
|
} else {
|
||||||
|
fileContent = await getFileStr(path)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsedYaml = yaml.load(fileContent)
|
||||||
|
if (privderType === 'proxy-providers') {
|
||||||
|
setCurrData(yaml.dump({
|
||||||
|
'proxies': parsedYaml[privderType][title].payload
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
setCurrData(yaml.dump({
|
||||||
|
'rules': parsedYaml[privderType][title].payload
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setCurrData(fileContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getContent()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
backdrop="blur"
|
||||||
|
classNames={{ backdrop: 'top-[48px]' }}
|
||||||
|
size="5xl"
|
||||||
|
hideCloseButton
|
||||||
|
isOpen={true}
|
||||||
|
onOpenChange={onClose}
|
||||||
|
scrollBehavior="inside"
|
||||||
|
>
|
||||||
|
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||||
|
<ModalHeader className="flex pb-0 app-drag">{title}</ModalHeader>
|
||||||
|
<ModalBody className="h-full">
|
||||||
|
<BaseEditor
|
||||||
|
language={language}
|
||||||
|
value={currData}
|
||||||
|
readOnly={type != 'File'}
|
||||||
|
onChange={(value) => setCurrData(value)}
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter className="pt-0">
|
||||||
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
|
{t('common.close')}
|
||||||
|
</Button>
|
||||||
|
{type == 'File' && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
onPress={async () => {
|
||||||
|
await setFileStr(path, currData)
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Viewer
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Card, CardBody } from '@nextui-org/react'
|
import { Card, CardBody } from '@heroui/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const RuleItem: React.FC<IMihomoRulesDetail & { index: number }> = (props) => {
|
const RuleItem: React.FC<IMihomoRulesDetail & { index: number }> = (props) => {
|
||||||
@ -10,7 +10,7 @@ const RuleItem: React.FC<IMihomoRulesDetail & { index: number }> = (props) => {
|
|||||||
<div title={payload} className="text-ellipsis whitespace-nowrap overflow-hidden">
|
<div title={payload} className="text-ellipsis whitespace-nowrap overflow-hidden">
|
||||||
{payload}
|
{payload}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-start text-default-500">
|
<div className="flex justify-start text-foreground-500">
|
||||||
<div>{type}</div>
|
<div>{type}</div>
|
||||||
<div className="ml-2">{proxy}</div>
|
<div className="ml-2">{proxy}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,18 +1,28 @@
|
|||||||
import { Button, Tooltip } from '@nextui-org/react'
|
import { Button, Tooltip } from '@heroui/react'
|
||||||
import SettingCard from '../base/base-setting-card'
|
import SettingCard from '../base/base-setting-card'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { checkUpdate, createHeapSnapshot, quitApp, quitWithoutCore } from '@renderer/utils/ipc'
|
import {
|
||||||
|
checkUpdate,
|
||||||
|
createHeapSnapshot,
|
||||||
|
quitApp,
|
||||||
|
quitWithoutCore,
|
||||||
|
resetAppConfig
|
||||||
|
} from '@renderer/utils/ipc'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import UpdaterModal from '../updater/updater-modal'
|
import UpdaterModal from '../updater/updater-modal'
|
||||||
import { version } from '@renderer/utils/init'
|
import { version } from '@renderer/utils/init'
|
||||||
import { IoIosHelpCircle } from 'react-icons/io'
|
import { IoIosHelpCircle } from 'react-icons/io'
|
||||||
import { firstDriver } from '@renderer/App'
|
import { getDriver } from '@renderer/App'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import BaseConfirmModal from '../base/base-confirm-modal'
|
||||||
|
|
||||||
const Actions: React.FC = () => {
|
const Actions: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [newVersion, setNewVersion] = useState('')
|
const [newVersion, setNewVersion] = useState('')
|
||||||
const [changelog, setChangelog] = useState('')
|
const [changelog, setChangelog] = useState('')
|
||||||
const [openUpdate, setOpenUpdate] = useState(false)
|
const [openUpdate, setOpenUpdate] = useState(false)
|
||||||
const [checkingUpdate, setCheckingUpdate] = useState(false)
|
const [checkingUpdate, setCheckingUpdate] = useState(false)
|
||||||
|
const [showResetConfirm, setShowResetConfirm] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -23,13 +33,25 @@ const Actions: React.FC = () => {
|
|||||||
changelog={changelog}
|
changelog={changelog}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showResetConfirm && (
|
||||||
|
<BaseConfirmModal
|
||||||
|
isOpen={showResetConfirm}
|
||||||
|
title={t('actions.reset.confirm.title')}
|
||||||
|
content={t('actions.reset.confirm.content')}
|
||||||
|
onCancel={() => setShowResetConfirm(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
resetAppConfig()
|
||||||
|
setShowResetConfirm(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingItem title="打开引导页面" divider>
|
<SettingItem title={t('actions.guide.title')} divider>
|
||||||
<Button size="sm" onPress={() => firstDriver.drive()}>
|
<Button size="sm" onPress={() => getDriver()?.drive()}>
|
||||||
打开引导页面
|
{t('actions.guide.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="检查更新" divider>
|
<SettingItem title={t('actions.update.title')} divider>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
isLoading={checkingUpdate}
|
isLoading={checkingUpdate}
|
||||||
@ -42,7 +64,9 @@ const Actions: React.FC = () => {
|
|||||||
setChangelog(version.changelog)
|
setChangelog(version.changelog)
|
||||||
setOpenUpdate(true)
|
setOpenUpdate(true)
|
||||||
} else {
|
} else {
|
||||||
new window.Notification('当前已是最新版本', { body: '无需更新' })
|
new window.Notification(t('actions.update.upToDate.title'), {
|
||||||
|
body: t('actions.update.upToDate.body')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e)
|
alert(e)
|
||||||
@ -51,13 +75,28 @@ const Actions: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
检查更新
|
{t('actions.update.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="创建堆快照"
|
title={t('actions.reset.title')}
|
||||||
actions={
|
actions={
|
||||||
<Tooltip content="创建主进程堆快照,用于排查内存问题">
|
<Tooltip content={t('actions.reset.tooltip')}>
|
||||||
|
<Button isIconOnly size="sm" variant="light">
|
||||||
|
<IoIosHelpCircle className="text-lg" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
divider
|
||||||
|
>
|
||||||
|
<Button size="sm" onPress={() => setShowResetConfirm(true)}>
|
||||||
|
{t('actions.reset.button')}
|
||||||
|
</Button>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem
|
||||||
|
title={t('actions.heapSnapshot.title')}
|
||||||
|
actions={
|
||||||
|
<Tooltip content={t('actions.heapSnapshot.tooltip')}>
|
||||||
<Button isIconOnly size="sm" variant="light">
|
<Button isIconOnly size="sm" variant="light">
|
||||||
<IoIosHelpCircle className="text-lg" />
|
<IoIosHelpCircle className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -66,13 +105,13 @@ const Actions: React.FC = () => {
|
|||||||
divider
|
divider
|
||||||
>
|
>
|
||||||
<Button size="sm" onPress={createHeapSnapshot}>
|
<Button size="sm" onPress={createHeapSnapshot}>
|
||||||
创建堆快照
|
{t('actions.heapSnapshot.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="轻量模式"
|
title={t('actions.lightMode.title')}
|
||||||
actions={
|
actions={
|
||||||
<Tooltip content="完全退出软件,只保留内核进程">
|
<Tooltip content={t('actions.lightMode.tooltip')}>
|
||||||
<Button isIconOnly size="sm" variant="light">
|
<Button isIconOnly size="sm" variant="light">
|
||||||
<IoIosHelpCircle className="text-lg" />
|
<IoIosHelpCircle className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -81,15 +120,15 @@ const Actions: React.FC = () => {
|
|||||||
divider
|
divider
|
||||||
>
|
>
|
||||||
<Button size="sm" onPress={quitWithoutCore}>
|
<Button size="sm" onPress={quitWithoutCore}>
|
||||||
轻量模式
|
{t('actions.lightMode.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="退出应用" divider>
|
<SettingItem title={t('actions.quit.title')} divider>
|
||||||
<Button size="sm" onPress={quitApp}>
|
<Button size="sm" onPress={quitApp}>
|
||||||
退出应用
|
{t('actions.quit.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="应用版本">
|
<SettingItem title={t('actions.version.title')}>
|
||||||
<div>v{version}</div>
|
<div>v{version}</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
||||||
import { BaseEditor } from '@renderer/components/base/base-editor'
|
import { BaseEditor } from '@renderer/components/base/base-editor'
|
||||||
import { readTheme } from '@renderer/utils/ipc'
|
import { readTheme } from '@renderer/utils/ipc'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
theme: string
|
theme: string
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
onConfirm: (script: string) => void
|
onConfirm: (script: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const CSSEditorModal: React.FC<Props> = (props) => {
|
const CSSEditorModal: React.FC<Props> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { theme, onCancel, onConfirm } = props
|
const { theme, onCancel, onConfirm } = props
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
|
|
||||||
@ -30,7 +34,7 @@ const CSSEditorModal: React.FC<Props> = (props) => {
|
|||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
>
|
>
|
||||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||||
<ModalHeader className="flex pb-0">编辑主题</ModalHeader>
|
<ModalHeader className="flex pb-0 app-drag">{t('theme.editor.title')}</ModalHeader>
|
||||||
<ModalBody className="h-full">
|
<ModalBody className="h-full">
|
||||||
<BaseEditor
|
<BaseEditor
|
||||||
language="css"
|
language="css"
|
||||||
@ -40,10 +44,10 @@ const CSSEditorModal: React.FC<Props> = (props) => {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter className="pt-0">
|
<ModalFooter className="pt-0">
|
||||||
<Button size="sm" variant="light" onPress={onCancel}>
|
<Button size="sm" variant="light" onPress={onCancel}>
|
||||||
取消
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" color="primary" onPress={() => onConfirm(currData)}>
|
<Button size="sm" color="primary" onPress={() => onConfirm(currData)}>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import SettingCard from '../base/base-setting-card'
|
import SettingCard from '../base/base-setting-card'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { Button, Input, Select, SelectItem, Switch, Tab, Tabs, Tooltip } from '@nextui-org/react'
|
import { Button, Input, Select, SelectItem, Switch, Tab, Tabs, Tooltip } from '@heroui/react'
|
||||||
import { BiCopy, BiSolidFileImport } from 'react-icons/bi'
|
import { BiCopy, BiSolidFileImport } from 'react-icons/bi'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import {
|
import {
|
||||||
applyTheme,
|
applyTheme,
|
||||||
checkAutoRun,
|
checkAutoRun,
|
||||||
|
closeFloatingWindow,
|
||||||
|
closeTrayIcon,
|
||||||
copyEnv,
|
copyEnv,
|
||||||
disableAutoRun,
|
disableAutoRun,
|
||||||
enableAutoRun,
|
enableAutoRun,
|
||||||
@ -15,35 +17,45 @@ import {
|
|||||||
importThemes,
|
importThemes,
|
||||||
relaunchApp,
|
relaunchApp,
|
||||||
resolveThemes,
|
resolveThemes,
|
||||||
restartCore,
|
showFloatingWindow,
|
||||||
|
showTrayIcon,
|
||||||
|
startMonitor,
|
||||||
writeTheme
|
writeTheme
|
||||||
} from '@renderer/utils/ipc'
|
} from '@renderer/utils/ipc'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
|
import debounce from '@renderer/utils/debounce'
|
||||||
import { platform } from '@renderer/utils/init'
|
import { platform } from '@renderer/utils/init'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { IoIosHelpCircle, IoMdCloudDownload } from 'react-icons/io'
|
import { IoIosHelpCircle, IoMdCloudDownload } from 'react-icons/io'
|
||||||
import { MdEditDocument } from 'react-icons/md'
|
import { MdEditDocument } from 'react-icons/md'
|
||||||
import CSSEditorModal from './css-editor-modal'
|
import CSSEditorModal from './css-editor-modal'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const GeneralConfig: React.FC = () => {
|
const GeneralConfig: React.FC = () => {
|
||||||
const { data: enable, mutate: mutateEnable } = useSWR('checkAutoRun', checkAutoRun)
|
const { t, i18n } = useTranslation()
|
||||||
|
const { data: enable = false, mutate: mutateEnable } = useSWR('checkAutoRun', checkAutoRun)
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const [customThemes, setCustomThemes] = useState<{ key: string; label: string }[]>()
|
const [customThemes, setCustomThemes] = useState<{ key: string; label: string }[]>()
|
||||||
const [openCSSEditor, setOpenCSSEditor] = useState(false)
|
const [openCSSEditor, setOpenCSSEditor] = useState(false)
|
||||||
const [fetching, setFetching] = useState(false)
|
const [fetching, setFetching] = useState(false)
|
||||||
|
const [isRelaunching, setIsRelaunching] = useState(false)
|
||||||
const { setTheme } = useTheme()
|
const { setTheme } = useTheme()
|
||||||
const {
|
const {
|
||||||
silentStart = false,
|
silentStart = false,
|
||||||
useDockIcon = true,
|
useDockIcon = true,
|
||||||
showTraffic = true,
|
showTraffic = false,
|
||||||
proxyInTray = true,
|
proxyInTray = true,
|
||||||
|
disableTray = false,
|
||||||
|
showFloatingWindow: showFloating = false,
|
||||||
|
spinFloatingIcon = true,
|
||||||
useWindowFrame = false,
|
useWindowFrame = false,
|
||||||
autoQuitWithoutCore = false,
|
autoQuitWithoutCore = false,
|
||||||
autoQuitWithoutCoreDelay = 60,
|
autoQuitWithoutCoreDelay = 60,
|
||||||
customTheme = 'default.css',
|
customTheme = 'default.css',
|
||||||
envType = [platform === 'win32' ? 'powershell' : 'bash'],
|
envType = [platform === 'win32' ? 'powershell' : 'bash'],
|
||||||
autoCheckUpdate,
|
autoCheckUpdate,
|
||||||
appTheme = 'system'
|
appTheme = 'system',
|
||||||
|
language = 'zh-CN'
|
||||||
} = appConfig || {}
|
} = appConfig || {}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -66,7 +78,26 @@ const GeneralConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingItem title="开机自启" divider>
|
<SettingItem title={t('settings.language')} divider>
|
||||||
|
<Select
|
||||||
|
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||||
|
className="w-[150px]"
|
||||||
|
size="sm"
|
||||||
|
selectedKeys={[language]}
|
||||||
|
aria-label={t('settings.language')}
|
||||||
|
onSelectionChange={async (v) => {
|
||||||
|
const newLang = Array.from(v)[0] as 'zh-CN' | 'en-US' | 'ru-RU' | 'fa-IR'
|
||||||
|
await patchAppConfig({ language: newLang })
|
||||||
|
i18n.changeLanguage(newLang)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectItem key="zh-CN">中文简体</SelectItem>
|
||||||
|
<SelectItem key="en-US">English</SelectItem>
|
||||||
|
<SelectItem key="ru-RU">Русский</SelectItem>
|
||||||
|
<SelectItem key="fa-IR">فارسی</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title={t('settings.autoStart')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={enable}
|
isSelected={enable}
|
||||||
@ -85,7 +116,7 @@ const GeneralConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="自动检查更新" divider>
|
<SettingItem title={t('settings.autoCheckUpdate')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={autoCheckUpdate}
|
isSelected={autoCheckUpdate}
|
||||||
@ -94,7 +125,7 @@ const GeneralConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="静默启动" divider>
|
<SettingItem title={t('settings.silentStart')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={silentStart}
|
isSelected={silentStart}
|
||||||
@ -104,9 +135,9 @@ const GeneralConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="自动开启轻量模式"
|
title={t('settings.autoQuitWithoutCore')}
|
||||||
actions={
|
actions={
|
||||||
<Tooltip content="关闭窗口指定时间后自动进入轻量模式">
|
<Tooltip content={t('settings.autoQuitWithoutCoreTooltip')}>
|
||||||
<Button isIconOnly size="sm" variant="light">
|
<Button isIconOnly size="sm" variant="light">
|
||||||
<IoIosHelpCircle className="text-lg" />
|
<IoIosHelpCircle className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -123,12 +154,12 @@ const GeneralConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
{autoQuitWithoutCore && (
|
{autoQuitWithoutCore && (
|
||||||
<SettingItem title="自动开启轻量模式延时" divider>
|
<SettingItem title={t('settings.autoQuitWithoutCoreDelay')} divider>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[100px]"
|
className="w-[100px]"
|
||||||
type="number"
|
type="number"
|
||||||
endContent="秒"
|
endContent={t('common.seconds')}
|
||||||
value={autoQuitWithoutCoreDelay.toString()}
|
value={autoQuitWithoutCoreDelay.toString()}
|
||||||
onValueChange={async (v: string) => {
|
onValueChange={async (v: string) => {
|
||||||
let num = parseInt(v)
|
let num = parseInt(v)
|
||||||
@ -140,7 +171,7 @@ const GeneralConfig: React.FC = () => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="复制环境变量类型"
|
title={t('settings.envType')}
|
||||||
actions={envType.map((type) => (
|
actions={envType.map((type) => (
|
||||||
<Button
|
<Button
|
||||||
key={type}
|
key={type}
|
||||||
@ -156,10 +187,13 @@ const GeneralConfig: React.FC = () => {
|
|||||||
divider
|
divider
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||||
className="w-[150px]"
|
className="w-[150px]"
|
||||||
size="sm"
|
size="sm"
|
||||||
selectionMode="multiple"
|
selectionMode="multiple"
|
||||||
selectedKeys={new Set(envType)}
|
selectedKeys={new Set(envType)}
|
||||||
|
aria-label={t('settings.envType')}
|
||||||
|
disallowEmptySelection={true}
|
||||||
onSelectionChange={async (v) => {
|
onSelectionChange={async (v) => {
|
||||||
try {
|
try {
|
||||||
await patchAppConfig({
|
await patchAppConfig({
|
||||||
@ -175,8 +209,52 @@ const GeneralConfig: React.FC = () => {
|
|||||||
<SelectItem key="powershell">PowerShell</SelectItem>
|
<SelectItem key="powershell">PowerShell</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem title={t('settings.showFloatingWindow')} divider>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={showFloating}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
await patchAppConfig({ showFloatingWindow: v })
|
||||||
|
if (v) {
|
||||||
|
showFloatingWindow()
|
||||||
|
} else {
|
||||||
|
closeFloatingWindow()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
{showFloating && (
|
||||||
|
<>
|
||||||
|
<SettingItem title={t('settings.spinFloatingIcon')} divider>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={spinFloatingIcon}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
await patchAppConfig({ spinFloatingIcon: v })
|
||||||
|
window.electron.ipcRenderer.send('updateFloatingWindow')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title={t('settings.disableTray')} divider>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={disableTray}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
await patchAppConfig({ disableTray: v })
|
||||||
|
if (v) {
|
||||||
|
closeTrayIcon()
|
||||||
|
} else {
|
||||||
|
showTrayIcon()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{platform !== 'linux' && (
|
{platform !== 'linux' && (
|
||||||
<SettingItem title="托盘菜单显示节点信息" divider>
|
<>
|
||||||
|
<SettingItem title={t('settings.proxyInTray')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={proxyInTray}
|
isSelected={proxyInTray}
|
||||||
@ -185,10 +263,26 @@ const GeneralConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem
|
||||||
|
title={t('settings.showTraffic', {
|
||||||
|
context: platform === 'win32' ? 'windows' : 'mac'
|
||||||
|
})}
|
||||||
|
divider
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={showTraffic}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
await patchAppConfig({ showTraffic: v })
|
||||||
|
await startMonitor()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{platform === 'darwin' && (
|
{platform === 'darwin' && (
|
||||||
<>
|
<>
|
||||||
<SettingItem title="显示 Dock 图标" divider>
|
<SettingItem title={t('settings.showDockIcon')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={useDockIcon}
|
isSelected={useDockIcon}
|
||||||
@ -197,30 +291,28 @@ const GeneralConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="显示网速信息" divider>
|
|
||||||
<Switch
|
|
||||||
size="sm"
|
|
||||||
isSelected={showTraffic}
|
|
||||||
onValueChange={async (v) => {
|
|
||||||
await patchAppConfig({ showTraffic: v })
|
|
||||||
await restartCore()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SettingItem title="使用系统标题栏" divider>
|
<SettingItem title={t('settings.useWindowFrame')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={useWindowFrame}
|
isSelected={useWindowFrame}
|
||||||
onValueChange={async (v) => {
|
isDisabled={isRelaunching}
|
||||||
|
onValueChange={debounce(async (v) => {
|
||||||
|
if (isRelaunching) return
|
||||||
|
setIsRelaunching(true)
|
||||||
|
try {
|
||||||
await patchAppConfig({ useWindowFrame: v })
|
await patchAppConfig({ useWindowFrame: v })
|
||||||
await relaunchApp()
|
await relaunchApp()
|
||||||
}}
|
} catch (e) {
|
||||||
|
alert(e)
|
||||||
|
setIsRelaunching(false)
|
||||||
|
}
|
||||||
|
}, 1000)}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="背景色" divider>
|
<SettingItem title={t('settings.backgroundColor')} divider>
|
||||||
<Tabs
|
<Tabs
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -230,20 +322,20 @@ const GeneralConfig: React.FC = () => {
|
|||||||
patchAppConfig({ appTheme: key as AppTheme })
|
patchAppConfig({ appTheme: key as AppTheme })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab key="system" title="自动" />
|
<Tab key="system" title={t('settings.backgroundAuto')} />
|
||||||
<Tab key="dark" title="深色" />
|
<Tab key="dark" title={t('settings.backgroundDark')} />
|
||||||
<Tab key="light" title="浅色" />
|
<Tab key="light" title={t('settings.backgroundLight')} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="主题"
|
title={t('settings.theme')}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
isLoading={fetching}
|
isLoading={fetching}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
title="拉取主题"
|
title={t('settings.fetchTheme')}
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setFetching(true)
|
setFetching(true)
|
||||||
@ -262,7 +354,7 @@ const GeneralConfig: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
title="导入主题"
|
title={t('settings.importTheme')}
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
const files = await getFilePath(['css'])
|
const files = await getFilePath(['css'])
|
||||||
@ -280,7 +372,7 @@ const GeneralConfig: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
title="编辑主题"
|
title={t('settings.editTheme')}
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setOpenCSSEditor(true)
|
setOpenCSSEditor(true)
|
||||||
@ -293,9 +385,12 @@ const GeneralConfig: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{customThemes && (
|
{customThemes && (
|
||||||
<Select
|
<Select
|
||||||
|
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||||
className="w-[60%]"
|
className="w-[60%]"
|
||||||
size="sm"
|
size="sm"
|
||||||
selectedKeys={new Set([customTheme])}
|
selectedKeys={new Set([customTheme])}
|
||||||
|
aria-label={t('settings.selectTheme')}
|
||||||
|
disallowEmptySelection={true}
|
||||||
onSelectionChange={async (v) => {
|
onSelectionChange={async (v) => {
|
||||||
try {
|
try {
|
||||||
await patchAppConfig({ customTheme: v.currentKey as string })
|
await patchAppConfig({ customTheme: v.currentKey as string })
|
||||||
|
|||||||
@ -1,16 +1,21 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import SettingCard from '../base/base-setting-card'
|
import SettingCard from '../base/base-setting-card'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { Button, Input, Select, SelectItem, Switch } from '@nextui-org/react'
|
import { Button, Input, Select, SelectItem, Switch, Tooltip } from '@heroui/react'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
import debounce from '@renderer/utils/debounce'
|
import debounce from '@renderer/utils/debounce'
|
||||||
import { getGistUrl, patchControledMihomoConfig, restartCore } from '@renderer/utils/ipc'
|
import { getGistUrl, patchControledMihomoConfig, restartCore } from '@renderer/utils/ipc'
|
||||||
import { MdDeleteForever } from 'react-icons/md'
|
import { MdDeleteForever } from 'react-icons/md'
|
||||||
import { BiCopy } from 'react-icons/bi'
|
import { BiCopy } from 'react-icons/bi'
|
||||||
|
import { IoIosHelpCircle } from 'react-icons/io'
|
||||||
|
import { platform, version } from '@renderer/utils/init'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const MihomoConfig: React.FC = () => {
|
const MihomoConfig: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const {
|
const {
|
||||||
|
diffWorkDir = false,
|
||||||
controlDns = true,
|
controlDns = true,
|
||||||
controlSniff = true,
|
controlSniff = true,
|
||||||
delayTestConcurrency,
|
delayTestConcurrency,
|
||||||
@ -20,6 +25,7 @@ const MihomoConfig: React.FC = () => {
|
|||||||
pauseSSID = [],
|
pauseSSID = [],
|
||||||
delayTestUrl,
|
delayTestUrl,
|
||||||
userAgent,
|
userAgent,
|
||||||
|
mihomoCpuPriority = 'PRIORITY_NORMAL',
|
||||||
proxyCols = 'auto'
|
proxyCols = 'auto'
|
||||||
} = appConfig || {}
|
} = appConfig || {}
|
||||||
const [url, setUrl] = useState(delayTestUrl)
|
const [url, setUrl] = useState(delayTestUrl)
|
||||||
@ -33,59 +39,59 @@ const MihomoConfig: React.FC = () => {
|
|||||||
}, 500)
|
}, 500)
|
||||||
return (
|
return (
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<SettingItem title="订阅拉取 UA" divider>
|
<SettingItem title={t('mihomo.userAgent')} divider>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[60%]"
|
className="w-[60%]"
|
||||||
value={ua}
|
value={ua}
|
||||||
placeholder="默认 clash.meta"
|
placeholder={t('mihomo.userAgentPlaceholder', { version })}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setUa(v)
|
setUa(v)
|
||||||
setUaDebounce(v)
|
setUaDebounce(v)
|
||||||
}}
|
}}
|
||||||
></Input>
|
></Input>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="延迟测试地址" divider>
|
<SettingItem title={t('mihomo.delayTest.url')} divider>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[60%]"
|
className="w-[60%]"
|
||||||
value={url}
|
value={url}
|
||||||
placeholder="默认 https://www.gstatic.com/generate_204"
|
placeholder={t('mihomo.delayTest.urlPlaceholder')}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setUrl(v)
|
setUrl(v)
|
||||||
setUrlDebounce(v)
|
setUrlDebounce(v)
|
||||||
}}
|
}}
|
||||||
></Input>
|
></Input>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="延迟测试并发数量" divider>
|
<SettingItem title={t('mihomo.delayTest.concurrency')} divider>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[60%]"
|
className="w-[60%]"
|
||||||
value={delayTestConcurrency?.toString()}
|
value={delayTestConcurrency?.toString()}
|
||||||
placeholder="默认 50"
|
placeholder={t('mihomo.delayTest.concurrencyPlaceholder')}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
patchAppConfig({ delayTestConcurrency: parseInt(v) })
|
patchAppConfig({ delayTestConcurrency: parseInt(v) })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="延迟测试超时时间" divider>
|
<SettingItem title={t('mihomo.delayTest.timeout')} divider>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[60%]"
|
className="w-[60%]"
|
||||||
value={delayTestTimeout?.toString()}
|
value={delayTestTimeout?.toString()}
|
||||||
placeholder="默认 5000"
|
placeholder={t('mihomo.delayTest.timeoutPlaceholder')}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
patchAppConfig({ delayTestTimeout: parseInt(v) })
|
patchAppConfig({ delayTestTimeout: parseInt(v) })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="同步运行时配置到 Gist"
|
title={t('mihomo.gist.title')}
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
title="复制 Gist URL"
|
title={t('mihomo.gist.copyUrl')}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
@ -110,29 +116,84 @@ const MihomoConfig: React.FC = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="w-[60%]"
|
className="w-[60%]"
|
||||||
value={githubToken}
|
value={githubToken}
|
||||||
placeholder="GitHub Token"
|
placeholder={t('mihomo.gist.token')}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
patchAppConfig({ githubToken: v })
|
patchAppConfig({ githubToken: v })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="代理节点展示列数" divider>
|
<SettingItem title={t('mihomo.proxyColumns.title')} divider>
|
||||||
<Select
|
<Select
|
||||||
|
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||||
className="w-[150px]"
|
className="w-[150px]"
|
||||||
size="sm"
|
size="sm"
|
||||||
selectedKeys={new Set([proxyCols])}
|
selectedKeys={new Set([proxyCols])}
|
||||||
|
aria-label={t('mihomo.proxyColumns.title')}
|
||||||
|
disallowEmptySelection={true}
|
||||||
onSelectionChange={async (v) => {
|
onSelectionChange={async (v) => {
|
||||||
await patchAppConfig({ proxyCols: v.currentKey as 'auto' | '1' | '2' | '3' | '4' })
|
await patchAppConfig({ proxyCols: v.currentKey as 'auto' | '1' | '2' | '3' | '4' })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectItem key="auto">自动</SelectItem>
|
<SelectItem key="auto">{t('mihomo.proxyColumns.auto')}</SelectItem>
|
||||||
<SelectItem key="1">一列</SelectItem>
|
<SelectItem key="1">{t('mihomo.proxyColumns.one')}</SelectItem>
|
||||||
<SelectItem key="2">两列</SelectItem>
|
<SelectItem key="2">{t('mihomo.proxyColumns.two')}</SelectItem>
|
||||||
<SelectItem key="3">三列</SelectItem>
|
<SelectItem key="3">{t('mihomo.proxyColumns.three')}</SelectItem>
|
||||||
<SelectItem key="4">四列</SelectItem>
|
<SelectItem key="4">{t('mihomo.proxyColumns.four')}</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="接管 DNS 设置" divider>
|
{platform === 'win32' && (
|
||||||
|
<SettingItem title={t('mihomo.cpuPriority.title')} divider>
|
||||||
|
<Select
|
||||||
|
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||||
|
className="w-[150px]"
|
||||||
|
size="sm"
|
||||||
|
selectedKeys={new Set([mihomoCpuPriority])}
|
||||||
|
disallowEmptySelection={true}
|
||||||
|
onSelectionChange={async (v) => {
|
||||||
|
try {
|
||||||
|
await patchAppConfig({
|
||||||
|
mihomoCpuPriority: v.currentKey as Priority
|
||||||
|
})
|
||||||
|
await restartCore()
|
||||||
|
} catch (e) {
|
||||||
|
alert(e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectItem key="PRIORITY_HIGHEST">{t('mihomo.cpuPriority.realtime')}</SelectItem>
|
||||||
|
<SelectItem key="PRIORITY_HIGH">{t('mihomo.cpuPriority.high')}</SelectItem>
|
||||||
|
<SelectItem key="PRIORITY_ABOVE_NORMAL">{t('mihomo.cpuPriority.aboveNormal')}</SelectItem>
|
||||||
|
<SelectItem key="PRIORITY_NORMAL">{t('mihomo.cpuPriority.normal')}</SelectItem>
|
||||||
|
<SelectItem key="PRIORITY_BELOW_NORMAL">{t('mihomo.cpuPriority.belowNormal')}</SelectItem>
|
||||||
|
<SelectItem key="PRIORITY_LOW">{t('mihomo.cpuPriority.low')}</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
|
)}
|
||||||
|
<SettingItem
|
||||||
|
title={t('mihomo.workDir.title')}
|
||||||
|
actions={
|
||||||
|
<Tooltip content={t('mihomo.workDir.tooltip')}>
|
||||||
|
<Button isIconOnly size="sm" variant="light">
|
||||||
|
<IoIosHelpCircle className="text-lg" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
divider
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={diffWorkDir}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
try {
|
||||||
|
await patchAppConfig({ diffWorkDir: v })
|
||||||
|
await restartCore()
|
||||||
|
} catch (e) {
|
||||||
|
alert(e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title={t('mihomo.controlDns')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={controlDns}
|
isSelected={controlDns}
|
||||||
@ -147,7 +208,7 @@ const MihomoConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="接管域名嗅探设置" divider>
|
<SettingItem title={t('mihomo.controlSniff')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={controlSniff}
|
isSelected={controlSniff}
|
||||||
@ -162,7 +223,7 @@ const MihomoConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="自动断开连接" divider>
|
<SettingItem title={t('mihomo.autoCloseConnection')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={autoCloseConnection}
|
isSelected={autoCloseConnection}
|
||||||
@ -171,7 +232,7 @@ const MihomoConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="在特定的 WiFi SSID 下直连">
|
<SettingItem title={t('mihomo.pauseSSID.title')}>
|
||||||
{pauseSSIDInput.join('') !== pauseSSID.join('') && (
|
{pauseSSIDInput.join('') !== pauseSSID.join('') && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -180,7 +241,7 @@ const MihomoConfig: React.FC = () => {
|
|||||||
patchAppConfig({ pauseSSID: pauseSSIDInput })
|
patchAppConfig({ pauseSSID: pauseSSIDInput })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
@ -207,7 +268,7 @@ const MihomoConfig: React.FC = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
color="warning"
|
color="warning"
|
||||||
onClick={() => setPauseSSIDInput(pauseSSIDInput.filter((_, i) => i !== index))}
|
onPress={() => setPauseSSIDInput(pauseSSIDInput.filter((_, i) => i !== index))}
|
||||||
>
|
>
|
||||||
<MdDeleteForever className="text-lg" />
|
<MdDeleteForever className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { Button, Input } from '@nextui-org/react'
|
import { Button, Input } from '@heroui/react'
|
||||||
import SettingCard from '../base/base-setting-card'
|
import SettingCard from '../base/base-setting-card'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
import React, { KeyboardEvent, useState } from 'react'
|
import React, { KeyboardEvent, useState } from 'react'
|
||||||
import { platform } from '@renderer/utils/init'
|
import { platform } from '@renderer/utils/init'
|
||||||
import { registerShortcut } from '@renderer/utils/ipc'
|
import { registerShortcut } from '@renderer/utils/ipc'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const keyMap = {
|
const keyMap = {
|
||||||
Backquote: '`',
|
Backquote: '`',
|
||||||
@ -40,9 +41,11 @@ const keyMap = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ShortcutConfig: React.FC = () => {
|
const ShortcutConfig: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const {
|
const {
|
||||||
showWindowShortcut = '',
|
showWindowShortcut = '',
|
||||||
|
showFloatingWindowShortcut = '',
|
||||||
triggerSysProxyShortcut = '',
|
triggerSysProxyShortcut = '',
|
||||||
triggerTunShortcut = '',
|
triggerTunShortcut = '',
|
||||||
ruleModeShortcut = '',
|
ruleModeShortcut = '',
|
||||||
@ -53,8 +56,8 @@ const ShortcutConfig: React.FC = () => {
|
|||||||
} = appConfig || {}
|
} = appConfig || {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingCard title="快捷键设置">
|
<SettingCard title={t('shortcuts.title')}>
|
||||||
<SettingItem title="打开/关闭窗口" divider>
|
<SettingItem title={t('shortcuts.toggleWindow')} divider>
|
||||||
<div className="flex justify-end w-[60%]">
|
<div className="flex justify-end w-[60%]">
|
||||||
<ShortcutInput
|
<ShortcutInput
|
||||||
value={showWindowShortcut}
|
value={showWindowShortcut}
|
||||||
@ -63,7 +66,16 @@ const ShortcutConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="打开/关闭系统代理" divider>
|
<SettingItem title={t('shortcuts.toggleFloatingWindow')} divider>
|
||||||
|
<div className="flex justify-end w-[60%]">
|
||||||
|
<ShortcutInput
|
||||||
|
value={showFloatingWindowShortcut}
|
||||||
|
patchAppConfig={patchAppConfig}
|
||||||
|
action="showFloatingWindowShortcut"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title={t('shortcuts.toggleSystemProxy')} divider>
|
||||||
<div className="flex justify-end w-[60%]">
|
<div className="flex justify-end w-[60%]">
|
||||||
<ShortcutInput
|
<ShortcutInput
|
||||||
value={triggerSysProxyShortcut}
|
value={triggerSysProxyShortcut}
|
||||||
@ -72,7 +84,7 @@ const ShortcutConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="打开/关闭虚拟网卡" divider>
|
<SettingItem title={t('shortcuts.toggleTun')} divider>
|
||||||
<div className="flex justify-end w-[60%]">
|
<div className="flex justify-end w-[60%]">
|
||||||
<ShortcutInput
|
<ShortcutInput
|
||||||
value={triggerTunShortcut}
|
value={triggerTunShortcut}
|
||||||
@ -81,7 +93,7 @@ const ShortcutConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="切换规则模式" divider>
|
<SettingItem title={t('shortcuts.toggleRuleMode')} divider>
|
||||||
<div className="flex justify-end w-[60%]">
|
<div className="flex justify-end w-[60%]">
|
||||||
<ShortcutInput
|
<ShortcutInput
|
||||||
value={ruleModeShortcut}
|
value={ruleModeShortcut}
|
||||||
@ -90,7 +102,7 @@ const ShortcutConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="切换全局模式" divider>
|
<SettingItem title={t('shortcuts.toggleGlobalMode')} divider>
|
||||||
<div className="flex justify-end w-[60%]">
|
<div className="flex justify-end w-[60%]">
|
||||||
<ShortcutInput
|
<ShortcutInput
|
||||||
value={globalModeShortcut}
|
value={globalModeShortcut}
|
||||||
@ -99,7 +111,7 @@ const ShortcutConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="切换直连模式" divider>
|
<SettingItem title={t('shortcuts.toggleDirectMode')} divider>
|
||||||
<div className="flex justify-end w-[60%]">
|
<div className="flex justify-end w-[60%]">
|
||||||
<ShortcutInput
|
<ShortcutInput
|
||||||
value={directModeShortcut}
|
value={directModeShortcut}
|
||||||
@ -108,7 +120,7 @@ const ShortcutConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="轻量模式" divider>
|
<SettingItem title={t('shortcuts.toggleLightMode')} divider>
|
||||||
<div className="flex justify-end w-[60%]">
|
<div className="flex justify-end w-[60%]">
|
||||||
<ShortcutInput
|
<ShortcutInput
|
||||||
value={quitWithoutCoreShortcut}
|
value={quitWithoutCoreShortcut}
|
||||||
@ -117,7 +129,7 @@ const ShortcutConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="重启应用">
|
<SettingItem title={t('shortcuts.restartApp')}>
|
||||||
<div className="flex justify-end w-[60%]">
|
<div className="flex justify-end w-[60%]">
|
||||||
<ShortcutInput
|
<ShortcutInput
|
||||||
value={restartAppShortcut}
|
value={restartAppShortcut}
|
||||||
@ -135,6 +147,7 @@ const ShortcutInput: React.FC<{
|
|||||||
action: string
|
action: string
|
||||||
patchAppConfig: (value: Partial<IAppConfig>) => Promise<void>
|
patchAppConfig: (value: Partial<IAppConfig>) => Promise<void>
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { value, action, patchAppConfig } = props
|
const { value, action, patchAppConfig } = props
|
||||||
const [inputValue, setInputValue] = useState(value)
|
const [inputValue, setInputValue] = useState(value)
|
||||||
|
|
||||||
@ -200,18 +213,18 @@ const ShortcutInput: React.FC<{
|
|||||||
await patchAppConfig({ [action]: inputValue })
|
await patchAppConfig({ [action]: inputValue })
|
||||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||||
} else {
|
} else {
|
||||||
alert('快捷键注册失败')
|
alert(t('common.error.shortcutRegistrationFailed'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(`快捷键注册失败: ${e}`)
|
alert(t('common.error.shortcutRegistrationFailedWithError', { error: e }))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
placeholder="点击输入快捷键"
|
placeholder={t('shortcuts.input.placeholder')}
|
||||||
onKeyDown={(e: KeyboardEvent): void => {
|
onKeyDown={(e: KeyboardEvent): void => {
|
||||||
parseShortcut(e, setInputValue)
|
parseShortcut(e, setInputValue)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,76 +1,73 @@
|
|||||||
import React from 'react'
|
import SettingCard from '@renderer/components/base/base-setting-card'
|
||||||
import SettingCard from '../base/base-setting-card'
|
import SettingItem from '@renderer/components/base/base-setting-item'
|
||||||
import SettingItem from '../base/base-setting-item'
|
|
||||||
import { RadioGroup, Radio } from '@nextui-org/react'
|
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
const titleMap = {
|
import { Radio, RadioGroup } from '@heroui/react'
|
||||||
sysproxyCardStatus: '系统代理',
|
import { useTranslation } from 'react-i18next'
|
||||||
tunCardStatus: '虚拟网卡',
|
import type { FC } from 'react'
|
||||||
profileCardStatus: '订阅管理',
|
|
||||||
proxyCardStatus: '代理组',
|
const titleMap: Record<string, string> = {
|
||||||
ruleCardStatus: '规则',
|
sysproxyCardStatus: 'sider.cards.systemProxy',
|
||||||
resourceCardStatus: '外部资源',
|
tunCardStatus: 'sider.cards.tun',
|
||||||
overrideCardStatus: '覆写',
|
profileCardStatus: 'sider.cards.profiles',
|
||||||
connectionCardStatus: '连接',
|
proxyCardStatus: 'sider.cards.proxies',
|
||||||
mihomoCoreCardStatus: '内核',
|
ruleCardStatus: 'sider.cards.rules',
|
||||||
dnsCardStatus: 'DNS',
|
resourceCardStatus: 'sider.cards.resources',
|
||||||
sniffCardStatus: '域名嗅探',
|
overrideCardStatus: 'sider.cards.override',
|
||||||
logCardStatus: '日志',
|
connectionCardStatus: 'sider.cards.connections',
|
||||||
substoreCardStatus: 'Sub-Store'
|
mihomoCoreCardStatus: 'sider.cards.core',
|
||||||
|
dnsCardStatus: 'sider.cards.dns',
|
||||||
|
sniffCardStatus: 'sider.cards.sniff',
|
||||||
|
logCardStatus: 'sider.cards.logs',
|
||||||
|
substoreCardStatus: 'sider.cards.substore'
|
||||||
}
|
}
|
||||||
const SiderConfig: React.FC = () => {
|
|
||||||
|
const sizeMap: Record<string, string> = {
|
||||||
|
'col-span-2': 'sider.size.large',
|
||||||
|
'col-span-1': 'sider.size.small',
|
||||||
|
hidden: 'sider.size.hidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SiderConfig: FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const {
|
|
||||||
sysproxyCardStatus = 'col-span-1',
|
|
||||||
tunCardStatus = 'col-span-1',
|
|
||||||
profileCardStatus = 'col-span-2',
|
|
||||||
proxyCardStatus = 'col-span-1',
|
|
||||||
ruleCardStatus = 'col-span-1',
|
|
||||||
resourceCardStatus = 'col-span-1',
|
|
||||||
overrideCardStatus = 'col-span-1',
|
|
||||||
connectionCardStatus = 'col-span-2',
|
|
||||||
mihomoCoreCardStatus = 'col-span-2',
|
|
||||||
dnsCardStatus = 'col-span-1',
|
|
||||||
sniffCardStatus = 'col-span-1',
|
|
||||||
logCardStatus = 'col-span-1',
|
|
||||||
substoreCardStatus = 'col-span-1'
|
|
||||||
} = appConfig || {}
|
|
||||||
|
|
||||||
const cardStatus = {
|
const cardStatus = {
|
||||||
sysproxyCardStatus,
|
sysproxyCardStatus: appConfig?.sysproxyCardStatus || 'col-span-1',
|
||||||
tunCardStatus,
|
tunCardStatus: appConfig?.tunCardStatus || 'col-span-1',
|
||||||
profileCardStatus,
|
profileCardStatus: appConfig?.profileCardStatus || 'col-span-2',
|
||||||
proxyCardStatus,
|
proxyCardStatus: appConfig?.proxyCardStatus || 'col-span-1',
|
||||||
ruleCardStatus,
|
ruleCardStatus: appConfig?.ruleCardStatus || 'col-span-1',
|
||||||
resourceCardStatus,
|
resourceCardStatus: appConfig?.resourceCardStatus || 'col-span-1',
|
||||||
overrideCardStatus,
|
overrideCardStatus: appConfig?.overrideCardStatus || 'col-span-1',
|
||||||
connectionCardStatus,
|
connectionCardStatus: appConfig?.connectionCardStatus || 'col-span-2',
|
||||||
mihomoCoreCardStatus,
|
mihomoCoreCardStatus: appConfig?.mihomoCoreCardStatus || 'col-span-2',
|
||||||
dnsCardStatus,
|
dnsCardStatus: appConfig?.dnsCardStatus || 'col-span-1',
|
||||||
sniffCardStatus,
|
sniffCardStatus: appConfig?.sniffCardStatus || 'col-span-1',
|
||||||
logCardStatus,
|
logCardStatus: appConfig?.logCardStatus || 'col-span-1',
|
||||||
substoreCardStatus
|
substoreCardStatus: appConfig?.substoreCardStatus || 'col-span-1'
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingCard title="侧边栏设置">
|
<SettingCard title={t('sider.title')}>
|
||||||
{Object.keys(cardStatus).map((key, index, array) => {
|
{Object.entries(cardStatus).map(([key, value]) => (
|
||||||
return (
|
<SettingItem key={key} title={t(titleMap[key])}>
|
||||||
<SettingItem title={titleMap[key]} key={key} divider={index !== array.length - 1}>
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
value={cardStatus[key]}
|
value={value}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v: string) => {
|
||||||
patchAppConfig({ [key]: v as CardStatus })
|
if (v === 'col-span-1' || v === 'col-span-2' || v === 'hidden') {
|
||||||
|
patchAppConfig({ [key]: v })
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Radio value="col-span-2">大</Radio>
|
{Object.entries(sizeMap).map(([size, label]) => (
|
||||||
<Radio value="col-span-1">小</Radio>
|
<Radio key={size} value={size}>
|
||||||
<Radio value="hidden">隐藏</Radio>
|
{t(label)}
|
||||||
|
</Radio>
|
||||||
|
))}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,26 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import SettingCard from '@renderer/components/base/base-setting-card'
|
import SettingCard from '@renderer/components/base/base-setting-card'
|
||||||
import SettingItem from '@renderer/components/base/base-setting-item'
|
import SettingItem from '@renderer/components/base/base-setting-item'
|
||||||
import { Button, Input, Switch } from '@nextui-org/react'
|
import { Button, Input, Switch } from '@heroui/react'
|
||||||
import { startSubStoreServer } from '@renderer/utils/ipc'
|
import {
|
||||||
|
startSubStoreFrontendServer,
|
||||||
|
startSubStoreBackendServer,
|
||||||
|
stopSubStoreFrontendServer,
|
||||||
|
stopSubStoreBackendServer
|
||||||
|
} from '@renderer/utils/ipc'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
import debounce from '@renderer/utils/debounce'
|
import debounce from '@renderer/utils/debounce'
|
||||||
import { isValidCron } from 'cron-validator'
|
import { isValidCron } from 'cron-validator'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const SubStoreConfig: React.FC = () => {
|
const SubStoreConfig: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const {
|
const {
|
||||||
useSubStore = true,
|
useSubStore = true,
|
||||||
useCustomSubStore = false,
|
useCustomSubStore = false,
|
||||||
|
useProxyInSubStore = false,
|
||||||
|
subStoreHost = '127.0.0.1',
|
||||||
customSubStoreUrl,
|
customSubStoreUrl,
|
||||||
subStoreBackendSyncCron,
|
subStoreBackendSyncCron,
|
||||||
subStoreBackendDownloadCron,
|
subStoreBackendDownloadCron,
|
||||||
@ -30,15 +39,21 @@ const SubStoreConfig: React.FC = () => {
|
|||||||
const [subStoreBackendUploadCronValue, setSubStoreBackendUploadCronValue] =
|
const [subStoreBackendUploadCronValue, setSubStoreBackendUploadCronValue] =
|
||||||
useState(subStoreBackendUploadCron)
|
useState(subStoreBackendUploadCron)
|
||||||
return (
|
return (
|
||||||
<SettingCard title="Sub-Store 设置">
|
<SettingCard title={t('substore.title')}>
|
||||||
<SettingItem title="启用 Sub-Store" divider>
|
<SettingItem title={t('substore.enable')} divider={useSubStore}>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={useSubStore}
|
isSelected={useSubStore}
|
||||||
onValueChange={async (v) => {
|
onValueChange={async (v) => {
|
||||||
try {
|
try {
|
||||||
await patchAppConfig({ useSubStore: v })
|
await patchAppConfig({ useSubStore: v })
|
||||||
if (v) await startSubStoreServer()
|
if (v) {
|
||||||
|
await startSubStoreFrontendServer()
|
||||||
|
await startSubStoreBackendServer()
|
||||||
|
} else {
|
||||||
|
await stopSubStoreFrontendServer()
|
||||||
|
await stopSubStoreBackendServer()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e)
|
alert(e)
|
||||||
}
|
}
|
||||||
@ -46,28 +61,51 @@ const SubStoreConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
{useSubStore && (
|
{useSubStore && (
|
||||||
<SettingItem title="使用自建 Sub-Store 后端" divider>
|
<>
|
||||||
|
<SettingItem title={t('substore.allowLan')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={useCustomSubStore}
|
isSelected={subStoreHost === '0.0.0.0'}
|
||||||
onValueChange={async (v) => {
|
onValueChange={async (v) => {
|
||||||
try {
|
try {
|
||||||
await patchAppConfig({ useCustomSubStore: v })
|
if (v) {
|
||||||
if (!v) await startSubStoreServer()
|
await patchAppConfig({ subStoreHost: '0.0.0.0' })
|
||||||
|
} else {
|
||||||
|
await patchAppConfig({ subStoreHost: '127.0.0.1' })
|
||||||
|
}
|
||||||
|
await startSubStoreFrontendServer()
|
||||||
|
await startSubStoreBackendServer()
|
||||||
|
} catch (e) {
|
||||||
|
alert(e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title={t('substore.useCustomBackend')} divider>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={useCustomSubStore}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
try {
|
||||||
|
await patchAppConfig({ useCustomSubStore: v })
|
||||||
|
if (v) {
|
||||||
|
await stopSubStoreBackendServer()
|
||||||
|
} else {
|
||||||
|
await startSubStoreBackendServer()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e)
|
alert(e)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
|
||||||
{useCustomSubStore ? (
|
{useCustomSubStore ? (
|
||||||
<SettingItem title="自建 Sub-Store 后端地址">
|
<SettingItem title={t('substore.customBackendUrl.title')}>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[60%]"
|
className="w-[60%]"
|
||||||
value={customSubStoreUrlValue}
|
value={customSubStoreUrlValue}
|
||||||
placeholder="必须包含协议头"
|
placeholder={t('substore.customBackendUrl.placeholder')}
|
||||||
onValueChange={(v: string) => {
|
onValueChange={(v: string) => {
|
||||||
setCustomSubStoreUrlValue(v)
|
setCustomSubStoreUrlValue(v)
|
||||||
setCustomSubStoreUrl(v)
|
setCustomSubStoreUrl(v)
|
||||||
@ -76,7 +114,21 @@ const SubStoreConfig: React.FC = () => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SettingItem title="定时同步订阅/文件" divider>
|
<SettingItem title={t('substore.useProxy')} divider>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={useProxyInSubStore}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
try {
|
||||||
|
await patchAppConfig({ useProxyInSubStore: v })
|
||||||
|
await startSubStoreBackendServer()
|
||||||
|
} catch (e) {
|
||||||
|
alert(e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title={t('substore.sync.title')} divider>
|
||||||
<div className="flex w-[60%] gap-2">
|
<div className="flex w-[60%] gap-2">
|
||||||
{subStoreBackendSyncCronValue !== subStoreBackendSyncCron && (
|
{subStoreBackendSyncCronValue !== subStoreBackendSyncCron && (
|
||||||
<Button
|
<Button
|
||||||
@ -90,27 +142,26 @@ const SubStoreConfig: React.FC = () => {
|
|||||||
await patchAppConfig({
|
await patchAppConfig({
|
||||||
subStoreBackendSyncCron: subStoreBackendSyncCronValue
|
subStoreBackendSyncCron: subStoreBackendSyncCronValue
|
||||||
})
|
})
|
||||||
new Notification('重启应用生效')
|
new Notification(t('common.notification.restartRequired'))
|
||||||
} else {
|
} else {
|
||||||
alert('Cron 表达式无效')
|
alert(t('common.error.invalidCron'))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-grown"
|
|
||||||
value={subStoreBackendSyncCronValue}
|
value={subStoreBackendSyncCronValue}
|
||||||
placeholder="Cron 表达式"
|
placeholder={t('substore.sync.placeholder')}
|
||||||
onValueChange={(v: string) => {
|
onValueChange={(v: string) => {
|
||||||
setSubStoreBackendSyncCronValue(v)
|
setSubStoreBackendSyncCronValue(v)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="定时恢复配置" divider>
|
<SettingItem title={t('substore.restore.title')} divider>
|
||||||
<div className="flex w-[60%] gap-2">
|
<div className="flex w-[60%] gap-2">
|
||||||
{subStoreBackendDownloadCronValue !== subStoreBackendDownloadCron && (
|
{subStoreBackendDownloadCronValue !== subStoreBackendDownloadCron && (
|
||||||
<Button
|
<Button
|
||||||
@ -124,27 +175,26 @@ const SubStoreConfig: React.FC = () => {
|
|||||||
await patchAppConfig({
|
await patchAppConfig({
|
||||||
subStoreBackendDownloadCron: subStoreBackendDownloadCronValue
|
subStoreBackendDownloadCron: subStoreBackendDownloadCronValue
|
||||||
})
|
})
|
||||||
new Notification('重启应用生效')
|
new Notification(t('common.notification.restartRequired'))
|
||||||
} else {
|
} else {
|
||||||
alert('Cron 表达式无效')
|
alert(t('common.error.invalidCron'))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-grown"
|
|
||||||
value={subStoreBackendDownloadCronValue}
|
value={subStoreBackendDownloadCronValue}
|
||||||
placeholder="Cron 表达式"
|
placeholder={t('substore.restore.placeholder')}
|
||||||
onValueChange={(v: string) => {
|
onValueChange={(v: string) => {
|
||||||
setSubStoreBackendDownloadCronValue(v)
|
setSubStoreBackendDownloadCronValue(v)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="定时备份配置">
|
<SettingItem title={t('substore.backup.title')}>
|
||||||
<div className="flex w-[60%] gap-2">
|
<div className="flex w-[60%] gap-2">
|
||||||
{subStoreBackendUploadCronValue !== subStoreBackendUploadCron && (
|
{subStoreBackendUploadCronValue !== subStoreBackendUploadCron && (
|
||||||
<Button
|
<Button
|
||||||
@ -158,20 +208,19 @@ const SubStoreConfig: React.FC = () => {
|
|||||||
await patchAppConfig({
|
await patchAppConfig({
|
||||||
subStoreBackendUploadCron: subStoreBackendUploadCronValue
|
subStoreBackendUploadCron: subStoreBackendUploadCronValue
|
||||||
})
|
})
|
||||||
new Notification('重启应用生效')
|
new Notification(t('common.notification.restartRequired'))
|
||||||
} else {
|
} else {
|
||||||
alert('Cron 表达式无效')
|
alert(t('common.error.invalidCron'))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-grown"
|
|
||||||
value={subStoreBackendUploadCronValue}
|
value={subStoreBackendUploadCronValue}
|
||||||
placeholder="Cron 表达式"
|
placeholder={t('substore.backup.placeholder')}
|
||||||
onValueChange={(v: string) => {
|
onValueChange={(v: string) => {
|
||||||
setSubStoreBackendUploadCronValue(v)
|
setSubStoreBackendUploadCronValue(v)
|
||||||
}}
|
}}
|
||||||
@ -180,6 +229,8 @@ const SubStoreConfig: React.FC = () => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,48 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import SettingCard from '../base/base-setting-card'
|
import SettingCard from '../base/base-setting-card'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { Button, Input } from '@nextui-org/react'
|
import { Button, Input, Select, SelectItem } from '@heroui/react'
|
||||||
import { listWebdavBackups, webdavBackup } from '@renderer/utils/ipc'
|
import { listWebdavBackups, webdavBackup } from '@renderer/utils/ipc'
|
||||||
import WebdavRestoreModal from './webdav-restore-modal'
|
import WebdavRestoreModal from './webdav-restore-modal'
|
||||||
import debounce from '@renderer/utils/debounce'
|
import debounce from '@renderer/utils/debounce'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const WebdavConfig: React.FC = () => {
|
const WebdavConfig: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const { webdavUrl, webdavUsername, webdavPassword } = appConfig || {}
|
const {
|
||||||
|
webdavUrl,
|
||||||
|
webdavUsername,
|
||||||
|
webdavPassword,
|
||||||
|
webdavDir = 'mihomo-party',
|
||||||
|
webdavMaxBackups = 0
|
||||||
|
} = appConfig || {}
|
||||||
const [backuping, setBackuping] = useState(false)
|
const [backuping, setBackuping] = useState(false)
|
||||||
const [restoring, setRestoring] = useState(false)
|
const [restoring, setRestoring] = useState(false)
|
||||||
const [filenames, setFilenames] = useState<string[]>([])
|
const [filenames, setFilenames] = useState<string[]>([])
|
||||||
const [restoreOpen, setRestoreOpen] = useState(false)
|
const [restoreOpen, setRestoreOpen] = useState(false)
|
||||||
|
|
||||||
const [webdav, setWebdav] = useState({ webdavUrl, webdavUsername, webdavPassword })
|
const [webdav, setWebdav] = useState({
|
||||||
const setWebdavDebounce = debounce(({ webdavUrl, webdavUsername, webdavPassword }) => {
|
webdavUrl,
|
||||||
patchAppConfig({ webdavUrl, webdavUsername, webdavPassword })
|
webdavUsername,
|
||||||
}, 500)
|
webdavPassword,
|
||||||
|
webdavDir,
|
||||||
|
webdavMaxBackups
|
||||||
|
})
|
||||||
|
const setWebdavDebounce = debounce(
|
||||||
|
({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups }) => {
|
||||||
|
patchAppConfig({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups })
|
||||||
|
},
|
||||||
|
500
|
||||||
|
)
|
||||||
const handleBackup = async (): Promise<void> => {
|
const handleBackup = async (): Promise<void> => {
|
||||||
setBackuping(true)
|
setBackuping(true)
|
||||||
try {
|
try {
|
||||||
await webdavBackup()
|
await webdavBackup()
|
||||||
new window.Notification('备份成功', { body: '备份文件已上传至 WebDav' })
|
new window.Notification(t('webdav.notification.backupSuccess.title'), {
|
||||||
|
body: t('webdav.notification.backupSuccess.body')
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e)
|
alert(e)
|
||||||
} finally {
|
} finally {
|
||||||
@ -38,7 +57,7 @@ const WebdavConfig: React.FC = () => {
|
|||||||
setFilenames(filenames)
|
setFilenames(filenames)
|
||||||
setRestoreOpen(true)
|
setRestoreOpen(true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(`获取备份列表失败: ${e}`)
|
alert(t('common.error.getBackupListFailed', { error: e }))
|
||||||
} finally {
|
} finally {
|
||||||
setRestoring(false)
|
setRestoring(false)
|
||||||
}
|
}
|
||||||
@ -48,8 +67,8 @@ const WebdavConfig: React.FC = () => {
|
|||||||
{restoreOpen && (
|
{restoreOpen && (
|
||||||
<WebdavRestoreModal filenames={filenames} onClose={() => setRestoreOpen(false)} />
|
<WebdavRestoreModal filenames={filenames} onClose={() => setRestoreOpen(false)} />
|
||||||
)}
|
)}
|
||||||
<SettingCard title="WebDav 备份">
|
<SettingCard title={t('webdav.title')}>
|
||||||
<SettingItem title="WebDav 地址" divider>
|
<SettingItem title={t('webdav.url')} divider>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[60%]"
|
className="w-[60%]"
|
||||||
@ -60,7 +79,18 @@ const WebdavConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="WebDav 用户名" divider>
|
<SettingItem title={t('webdav.dir')} divider>
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
className="w-[60%]"
|
||||||
|
value={webdav.webdavDir}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setWebdav({ ...webdav, webdavDir: v })
|
||||||
|
setWebdavDebounce({ ...webdav, webdavDir: v })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title={t('webdav.username')} divider>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[60%]"
|
className="w-[60%]"
|
||||||
@ -71,7 +101,7 @@ const WebdavConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title="WebDav 密码" divider>
|
<SettingItem title={t('webdav.password')} divider>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-[60%]"
|
className="w-[60%]"
|
||||||
@ -83,9 +113,31 @@ const WebdavConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem title={t('webdav.maxBackups')} divider>
|
||||||
|
<Select
|
||||||
|
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||||
|
className="w-[150px]"
|
||||||
|
size="sm"
|
||||||
|
selectedKeys={new Set([webdav.webdavMaxBackups.toString()])}
|
||||||
|
aria-label={t('webdav.maxBackups')}
|
||||||
|
onSelectionChange={(v) => {
|
||||||
|
const value = Number.parseInt(Array.from(v)[0] as string, 10)
|
||||||
|
setWebdav({ ...webdav, webdavMaxBackups: value })
|
||||||
|
setWebdavDebounce({ ...webdav, webdavMaxBackups: value })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectItem key="0">{t('webdav.noLimit')}</SelectItem>
|
||||||
|
<SelectItem key="1">1</SelectItem>
|
||||||
|
<SelectItem key="3">3</SelectItem>
|
||||||
|
<SelectItem key="5">5</SelectItem>
|
||||||
|
<SelectItem key="10">10</SelectItem>
|
||||||
|
<SelectItem key="15">15</SelectItem>
|
||||||
|
<SelectItem key="20">20</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
<div className="flex justify0between">
|
<div className="flex justify0between">
|
||||||
<Button isLoading={backuping} fullWidth size="sm" className="mr-1" onPress={handleBackup}>
|
<Button isLoading={backuping} fullWidth size="sm" className="mr-1" onPress={handleBackup}>
|
||||||
备份
|
{t('webdav.backup')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isLoading={restoring}
|
isLoading={restoring}
|
||||||
@ -94,7 +146,7 @@ const WebdavConfig: React.FC = () => {
|
|||||||
className="ml-1"
|
className="ml-1"
|
||||||
onPress={handleRestore}
|
onPress={handleRestore}
|
||||||
>
|
>
|
||||||
恢复
|
{t('webdav.restore.title')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
||||||
import { relaunchApp, webdavDelete, webdavRestore } from '@renderer/utils/ipc'
|
import { relaunchApp, webdavDelete, webdavRestore } from '@renderer/utils/ipc'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { MdDeleteForever } from 'react-icons/md'
|
import { MdDeleteForever } from 'react-icons/md'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filenames: string[]
|
filenames: string[]
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const WebdavRestoreModal: React.FC<Props> = (props) => {
|
const WebdavRestoreModal: React.FC<Props> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { filenames: names, onClose } = props
|
const { filenames: names, onClose } = props
|
||||||
const [filenames, setFilenames] = useState<string[]>(names)
|
const [filenames, setFilenames] = useState<string[]>(names)
|
||||||
const [restoring, setRestoring] = useState(false)
|
const [restoring, setRestoring] = useState(false)
|
||||||
@ -21,10 +25,10 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
|
|||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader className="flex">恢复备份</ModalHeader>
|
<ModalHeader className="flex app-drag">{t('webdav.restore.title')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{filenames.length === 0 ? (
|
{filenames.length === 0 ? (
|
||||||
<div className="flex justify-center">还没有备份</div>
|
<div className="flex justify-center">{t('webdav.restore.noBackups')}</div>
|
||||||
) : (
|
) : (
|
||||||
filenames.map((filename) => (
|
filenames.map((filename) => (
|
||||||
<div className="flex" key={filename}>
|
<div className="flex" key={filename}>
|
||||||
@ -39,7 +43,7 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
|
|||||||
await webdavRestore(filename)
|
await webdavRestore(filename)
|
||||||
await relaunchApp()
|
await relaunchApp()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(`恢复失败: ${e}`)
|
alert(t('common.error.restoreFailed', { error: e }))
|
||||||
} finally {
|
} finally {
|
||||||
setRestoring(false)
|
setRestoring(false)
|
||||||
}
|
}
|
||||||
@ -52,12 +56,12 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
|
|||||||
color="warning"
|
color="warning"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
onClick={async () => {
|
onPress={async () => {
|
||||||
try {
|
try {
|
||||||
await webdavDelete(filename)
|
await webdavDelete(filename)
|
||||||
setFilenames(filenames.filter((name) => name !== filename))
|
setFilenames(filenames.filter((name) => name !== filename))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(`删除失败: ${e}`)
|
alert(t('common.error.deleteFailed', { error: e }))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -68,8 +72,8 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="light" onPress={onClose}>
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
关闭
|
{t('common.close')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { BaseEditor } from '../base/base-editor'
|
import { BaseEditor } from '../base/base-editor'
|
||||||
import { getRuntimeConfigStr } from '@renderer/utils/ipc'
|
import { getRuntimeConfigStr } from '@renderer/utils/ipc'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
const ConfigViewer: React.FC<Props> = (props) => {
|
const ConfigViewer: React.FC<Props> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { onClose } = props
|
const { onClose } = props
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
|
|
||||||
@ -28,13 +31,13 @@ const ConfigViewer: React.FC<Props> = (props) => {
|
|||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
>
|
>
|
||||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||||
<ModalHeader className="flex pb-0">当前运行时配置</ModalHeader>
|
<ModalHeader className="flex pb-0 app-drag">{t('sider.cards.config')}</ModalHeader>
|
||||||
<ModalBody className="h-full">
|
<ModalBody className="h-full">
|
||||||
<BaseEditor language="yaml" value={currData} readOnly={true} />
|
<BaseEditor language="yaml" value={currData} readOnly={true} />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter className="pt-0">
|
<ModalFooter className="pt-0">
|
||||||
<Button size="sm" variant="light" onPress={onClose}>
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
关闭
|
{t('common.close')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
|
import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
|
||||||
import { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6'
|
import { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { calcTraffic } from '@renderer/utils/calc'
|
import { calcTraffic } from '@renderer/utils/calc'
|
||||||
import { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useSortable } from '@dnd-kit/sortable'
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { IoLink } from 'react-icons/io5'
|
import { IoLink } from 'react-icons/io5'
|
||||||
@ -10,18 +10,25 @@ import { useTheme } from 'next-themes'
|
|||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
import { platform } from '@renderer/utils/init'
|
import { platform } from '@renderer/utils/init'
|
||||||
import { Area, AreaChart, ResponsiveContainer } from 'recharts'
|
import { Area, AreaChart, ResponsiveContainer } from 'recharts'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
let currentUpload: number | undefined = undefined
|
let currentUpload: number | undefined = undefined
|
||||||
let currentDownload: number | undefined = undefined
|
let currentDownload: number | undefined = undefined
|
||||||
let hasShowTraffic = false
|
let hasShowTraffic = false
|
||||||
let drawing = false
|
let drawing = false
|
||||||
|
|
||||||
const ConnCard: React.FC = () => {
|
interface Props {
|
||||||
|
iconOnly?: boolean
|
||||||
|
}
|
||||||
|
const ConnCard: React.FC<Props> = (props) => {
|
||||||
const { theme = 'system', systemTheme = 'dark' } = useTheme()
|
const { theme = 'system', systemTheme = 'dark' } = useTheme()
|
||||||
|
const { iconOnly } = props
|
||||||
const { appConfig } = useAppConfig()
|
const { appConfig } = useAppConfig()
|
||||||
const { showTraffic, connectionCardStatus = 'col-span-2', customTheme } = appConfig || {}
|
const { showTraffic = false, connectionCardStatus = 'col-span-2', customTheme } = appConfig || {}
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
const match = location.pathname.includes('/connections')
|
const match = location.pathname.includes('/connections')
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [upload, setUpload] = useState(0)
|
const [upload, setUpload] = useState(0)
|
||||||
const [download, setDownload] = useState(0)
|
const [download, setDownload] = useState(0)
|
||||||
@ -39,17 +46,22 @@ const ConnCard: React.FC = () => {
|
|||||||
const [chartColor, setChartColor] = useState('rgba(255,255,255)')
|
const [chartColor, setChartColor] = useState('rgba(255,255,255)')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
|
||||||
const islight = theme === 'system' ? systemTheme === 'light' : theme.includes('light')
|
|
||||||
setChartColor(
|
setChartColor(
|
||||||
match
|
match
|
||||||
? 'rgba(255,255,255)'
|
? `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-primary-foreground')})`
|
||||||
: islight
|
: `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-foreground')})`
|
||||||
? window.getComputedStyle(document.documentElement).color
|
|
||||||
: 'rgb(255,255,255)'
|
|
||||||
)
|
)
|
||||||
}, 1000)
|
}, [theme, systemTheme, match])
|
||||||
}, [theme, systemTheme, match, customTheme])
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setChartColor(
|
||||||
|
match
|
||||||
|
? `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-primary-foreground')})`
|
||||||
|
: `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-foreground')})`
|
||||||
|
)
|
||||||
|
}, 200)
|
||||||
|
}, [customTheme])
|
||||||
|
|
||||||
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -82,6 +94,26 @@ const ConnCard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [showTraffic])
|
}, [showTraffic])
|
||||||
|
|
||||||
|
if (iconOnly) {
|
||||||
|
return (
|
||||||
|
<div className={`${connectionCardStatus} flex justify-center`}>
|
||||||
|
<Tooltip content={t('sider.cards.connections')} placement="right">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
color={match ? 'primary' : 'default'}
|
||||||
|
variant={match ? 'solid' : 'light'}
|
||||||
|
onPress={() => {
|
||||||
|
navigate('/connections')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IoLink className="text-[20px]" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -90,7 +122,7 @@ const ConnCard: React.FC = () => {
|
|||||||
transition,
|
transition,
|
||||||
zIndex: isDragging ? 'calc(infinity)' : undefined
|
zIndex: isDragging ? 'calc(infinity)' : undefined
|
||||||
}}
|
}}
|
||||||
className={connectionCardStatus}
|
className={`${connectionCardStatus} conn-card`}
|
||||||
>
|
>
|
||||||
{connectionCardStatus === 'col-span-2' ? (
|
{connectionCardStatus === 'col-span-2' ? (
|
||||||
<>
|
<>
|
||||||
@ -111,10 +143,12 @@ const ConnCard: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<IoLink
|
<IoLink
|
||||||
color="default"
|
color="default"
|
||||||
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
|
className={`${match ? 'text-primary-foreground' : 'text-foreground'} text-[24px]`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<div className={`p-2 w-full ${match ? 'text-white' : 'text-foreground'} `}>
|
<div
|
||||||
|
className={`p-2 w-full ${match ? 'text-primary-foreground' : 'text-foreground'} `}
|
||||||
|
>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="w-full text-right mr-2">{calcTraffic(upload)}/s</div>
|
<div className="w-full text-right mr-2">{calcTraffic(upload)}/s</div>
|
||||||
<FaCircleArrowUp className="h-[24px] leading-[24px]" />
|
<FaCircleArrowUp className="h-[24px] leading-[24px]" />
|
||||||
@ -127,8 +161,10 @@ const ConnCard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="pt-1">
|
<CardFooter className="pt-1">
|
||||||
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
|
<h3
|
||||||
连接
|
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
|
>
|
||||||
|
{t('sider.cards.connections')}
|
||||||
</h3>
|
</h3>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
@ -175,14 +211,16 @@ const ConnCard: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<IoLink
|
<IoLink
|
||||||
color="default"
|
color="default"
|
||||||
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
|
className={`${match ? 'text-primary-foreground' : 'text-foreground'} text-[24px] font-bold`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="pt-1">
|
<CardFooter className="pt-1">
|
||||||
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
|
<h3
|
||||||
连接
|
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
|
>
|
||||||
|
{t('sider.cards.connections')}
|
||||||
</h3>
|
</h3>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,16 +1,25 @@
|
|||||||
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
|
import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
|
||||||
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
|
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
|
||||||
import BorderSwitch from '@renderer/components/base/border-swtich'
|
import BorderSwitch from '@renderer/components/base/border-swtich'
|
||||||
import { LuServer } from 'react-icons/lu'
|
import { LuServer } from 'react-icons/lu'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { patchMihomoConfig } from '@renderer/utils/ipc'
|
import { patchMihomoConfig } from '@renderer/utils/ipc'
|
||||||
import { useSortable } from '@dnd-kit/sortable'
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
const DNSCard: React.FC = () => {
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
iconOnly?: boolean
|
||||||
|
}
|
||||||
|
const DNSCard: React.FC<Props> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { appConfig } = useAppConfig()
|
const { appConfig } = useAppConfig()
|
||||||
|
const { iconOnly } = props
|
||||||
const { dnsCardStatus = 'col-span-1', controlDns = true } = appConfig || {}
|
const { dnsCardStatus = 'col-span-1', controlDns = true } = appConfig || {}
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
const match = location.pathname.includes('/dns')
|
const match = location.pathname.includes('/dns')
|
||||||
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
|
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
|
||||||
const { dns, tun } = controledMihomoConfig || {}
|
const { dns, tun } = controledMihomoConfig || {}
|
||||||
@ -31,6 +40,26 @@ const DNSCard: React.FC = () => {
|
|||||||
await patchMihomoConfig({ dns: { enable } })
|
await patchMihomoConfig({ dns: { enable } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (iconOnly) {
|
||||||
|
return (
|
||||||
|
<div className={`${dnsCardStatus} ${!controlDns ? 'hidden' : ''} flex justify-center`}>
|
||||||
|
<Tooltip content={t('sider.cards.dns')} placement="right">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
color={match ? 'primary' : 'default'}
|
||||||
|
variant={match ? 'solid' : 'light'}
|
||||||
|
onPress={() => {
|
||||||
|
navigate('/dns')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LuServer className="text-[20px]" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -57,7 +86,7 @@ const DNSCard: React.FC = () => {
|
|||||||
color="default"
|
color="default"
|
||||||
>
|
>
|
||||||
<LuServer
|
<LuServer
|
||||||
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
|
className={`${match ? 'text-primary-foreground' : 'text-foreground'} text-[24px] font-bold`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<BorderSwitch
|
<BorderSwitch
|
||||||
@ -69,7 +98,11 @@ const DNSCard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="pt-1">
|
<CardFooter className="pt-1">
|
||||||
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>DNS</h3>
|
<h3
|
||||||
|
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
|
>
|
||||||
|
{t('sider.cards.dns')}
|
||||||
|
</h3>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,23 @@
|
|||||||
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
|
import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
|
||||||
import { IoJournalOutline } from 'react-icons/io5'
|
import { IoJournalOutline } from 'react-icons/io5'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { useSortable } from '@dnd-kit/sortable'
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
const LogCard: React.FC = () => {
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
iconOnly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogCard: React.FC<Props> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { appConfig } = useAppConfig()
|
const { appConfig } = useAppConfig()
|
||||||
|
const { iconOnly } = props
|
||||||
const { logCardStatus = 'col-span-1' } = appConfig || {}
|
const { logCardStatus = 'col-span-1' } = appConfig || {}
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
const match = location.pathname.includes('/logs')
|
const match = location.pathname.includes('/logs')
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
@ -20,6 +30,26 @@ const LogCard: React.FC = () => {
|
|||||||
id: 'log'
|
id: 'log'
|
||||||
})
|
})
|
||||||
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
||||||
|
|
||||||
|
if (iconOnly) {
|
||||||
|
return (
|
||||||
|
<div className={`${logCardStatus} flex justify-center`}>
|
||||||
|
<Tooltip content={t('sider.cards.logs')} placement="right">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
color={match ? 'primary' : 'default'}
|
||||||
|
variant={match ? 'solid' : 'light'}
|
||||||
|
onPress={() => {
|
||||||
|
navigate('/logs')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IoJournalOutline className="text-[20px]" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -47,13 +77,17 @@ const LogCard: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<IoJournalOutline
|
<IoJournalOutline
|
||||||
color="default"
|
color="default"
|
||||||
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
|
className={`${match ? 'text-primary-foreground' : 'text-foreground'} text-[24px] font-bold`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="pt-1">
|
<CardFooter className="pt-1">
|
||||||
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>日志</h3>
|
<h3
|
||||||
|
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
|
>
|
||||||
|
{t('sider.cards.logs')}
|
||||||
|
</h3>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,21 +1,28 @@
|
|||||||
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
|
import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
|
||||||
import { calcTraffic } from '@renderer/utils/calc'
|
import { calcTraffic } from '@renderer/utils/calc'
|
||||||
import { mihomoVersion, restartCore } from '@renderer/utils/ipc'
|
import { mihomoVersion, restartCore } from '@renderer/utils/ipc'
|
||||||
import { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { IoMdRefresh } from 'react-icons/io'
|
import { IoMdRefresh } from 'react-icons/io'
|
||||||
import { useSortable } from '@dnd-kit/sortable'
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import PubSub from 'pubsub-js'
|
import PubSub from 'pubsub-js'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
import { LuCpu } from 'react-icons/lu'
|
import { LuCpu } from 'react-icons/lu'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const MihomoCoreCard: React.FC = () => {
|
interface Props {
|
||||||
|
iconOnly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const MihomoCoreCard: React.FC<Props> = (props) => {
|
||||||
const { appConfig } = useAppConfig()
|
const { appConfig } = useAppConfig()
|
||||||
|
const { iconOnly } = props
|
||||||
const { mihomoCoreCardStatus = 'col-span-2' } = appConfig || {}
|
const { mihomoCoreCardStatus = 'col-span-2' } = appConfig || {}
|
||||||
const { data: version, mutate } = useSWR('mihomoVersion', mihomoVersion)
|
const { data: version, mutate } = useSWR('mihomoVersion', mihomoVersion)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
const match = location.pathname.includes('/mihomo')
|
const match = location.pathname.includes('/mihomo')
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
@ -29,6 +36,7 @@ const MihomoCoreCard: React.FC = () => {
|
|||||||
})
|
})
|
||||||
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
||||||
const [mem, setMem] = useState(0)
|
const [mem, setMem] = useState(0)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = PubSub.subscribe('mihomo-core-changed', () => {
|
const token = PubSub.subscribe('mihomo-core-changed', () => {
|
||||||
@ -43,6 +51,26 @@ const MihomoCoreCard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
if (iconOnly) {
|
||||||
|
return (
|
||||||
|
<div className={`${mihomoCoreCardStatus} flex justify-center`}>
|
||||||
|
<Tooltip content={t('sider.cards.core')} placement="right">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
color={match ? 'primary' : 'default'}
|
||||||
|
variant={match ? 'solid' : 'light'}
|
||||||
|
onPress={() => {
|
||||||
|
navigate('/mihomo')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LuCpu className="text-[20px]" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -51,7 +79,7 @@ const MihomoCoreCard: React.FC = () => {
|
|||||||
transition,
|
transition,
|
||||||
zIndex: isDragging ? 'calc(infinity)' : undefined
|
zIndex: isDragging ? 'calc(infinity)' : undefined
|
||||||
}}
|
}}
|
||||||
className={mihomoCoreCardStatus}
|
className={`${mihomoCoreCardStatus} mihomo-core-card`}
|
||||||
>
|
>
|
||||||
{mihomoCoreCardStatus === 'col-span-2' ? (
|
{mihomoCoreCardStatus === 'col-span-2' ? (
|
||||||
<Card
|
<Card
|
||||||
@ -69,7 +97,7 @@ const MihomoCoreCard: React.FC = () => {
|
|||||||
className="flex justify-between h-[32px]"
|
className="flex justify-between h-[32px]"
|
||||||
>
|
>
|
||||||
<h3
|
<h3
|
||||||
className={`text-md font-bold leading-[32px] ${match ? 'text-white' : 'text-foreground'} `}
|
className={`text-md font-bold leading-[32px] ${match ? 'text-primary-foreground' : 'text-foreground'} `}
|
||||||
>
|
>
|
||||||
{version?.version ?? '-'}
|
{version?.version ?? '-'}
|
||||||
</h3>
|
</h3>
|
||||||
@ -90,16 +118,16 @@ const MihomoCoreCard: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IoMdRefresh
|
<IoMdRefresh
|
||||||
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
|
className={`${match ? 'text-primary-foreground' : 'text-foreground'} text-[24px]`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="pt-1">
|
<CardFooter className="pt-1">
|
||||||
<div
|
<div
|
||||||
className={`flex justify-between w-full text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}
|
className={`flex justify-between w-full text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
>
|
>
|
||||||
<h4>内核设置</h4>
|
<h4>{t('sider.cards.core')}</h4>
|
||||||
<h4>{calcTraffic(mem)}</h4>
|
<h4>{calcTraffic(mem)}</h4>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@ -122,14 +150,16 @@ const MihomoCoreCard: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<LuCpu
|
<LuCpu
|
||||||
color="default"
|
color="default"
|
||||||
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
|
className={`${match ? 'text-primary-foreground' : 'text-foreground'} text-[24px] font-bold`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="pt-1">
|
<CardFooter className="pt-1">
|
||||||
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
|
<h3
|
||||||
内核设置
|
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
|
>
|
||||||
|
{t('sider.cards.core')}
|
||||||
</h3>
|
</h3>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user