Compare commits

...

23 Commits

55 changed files with 1707 additions and 1084 deletions

View File

@@ -24,7 +24,6 @@ jobs:
os: ubuntu-22.04 os: ubuntu-22.04
runs-on: ${{ matrix.targets.os }} runs-on: ${{ matrix.targets.os }}
if: startsWith(github.repository, 'wonfen')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -17,7 +17,6 @@ jobs:
os: [windows-latest, ubuntu-latest, macos-latest] os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
if: startsWith(github.repository, 'wonfen')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -17,7 +17,6 @@ jobs:
matrix: matrix:
os: [windows-latest, ubuntu-latest, macos-latest] os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
if: startsWith(github.repository, 'wonfen')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -5,7 +5,7 @@ on:
push: push:
tags: tags:
- v** - v**
permissions: write-all
env: env:
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short RUST_BACKTRACE: short
@@ -15,17 +15,36 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [windows-latest, ubuntu-latest, macos-latest] include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: i686-pc-windows-msvc
# - os: windows-latest
# target: aarch64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
# - os: ubuntu-latest
# target: armv7-unknown-linux-gnueabihf
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
if: startsWith(github.repository, 'wonfen')
steps: steps:
- name: Checkout repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: install Rust stable - name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }}
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
@@ -43,17 +62,42 @@ jobs:
run_install: false run_install: false
- name: Install Dependencies (Ubuntu Only) - name: Install Dependencies (Ubuntu Only)
if: startsWith(matrix.os, 'ubuntu-') if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.target,'x86_64')
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libayatana-appindicator3-dev librsvg2-dev patchelf openssl
- name: Install Dependencies (Ubuntu Only)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.target,'aarch64')
run: |
sed 's/mirror+file:\/etc\/apt\/apt-mirrors.txt/[arch-=amd64] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | sudo tee /etc/apt/sources.list.d/ports.list
sudo sed -i 's/mirror+file/[arch=amd64] mirror+file/g' /etc/apt/sources.list
cat /etc/apt/sources.list
cat /etc/apt/sources.list.d/ports.list
sudo dpkg --add-architecture arm64
sudo apt-get update
sudo apt-get install -y libncurses6:arm64 libtinfo6:arm64 linux-libc-dev:arm64 libncursesw6:arm64 libssl3:arm64 libcups2:arm64
sudo apt-get install -y --no-install-recommends g++-aarch64-linux-gnu libc6-dev-arm64-cross libssl-dev:arm64 libwebkit2gtk-4.0-dev:arm64 libgtk-3-dev:arm64 patchelf:arm64 librsvg2-dev:arm64 libayatana-appindicator3-dev:arm64
- name: Install Dependencies (Ubuntu Only)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.target,'armv7')
run: |
sed 's/mirror+file:\/etc\/apt\/apt-mirrors.txt/[arch-=amd64] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | sudo tee /etc/apt/sources.list.d/ports.list
sudo sed -i 's/mirror+file/[arch=amd64] mirror+file/g' /etc/apt/sources.list
cat /etc/apt/sources.list
cat /etc/apt/sources.list.d/ports.list
sudo dpkg --add-architecture armhf
sudo apt-get update
sudo apt-get install -y libncurses6:armhf libtinfo6:armhf linux-libc-dev:armhf libncursesw6:armhf libssl3:armhf libcups2:armhf
sudo apt-get install -y --no-install-recommends g++-arm-linux-gnueabihf libc6-dev-armhf-cross libssl-dev:armhf libwebkit2gtk-4.0-dev:armhf libgtk-3-dev:armhf patchelf:armhf librsvg2-dev:armhf libayatana-appindicator3-dev:armhf
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm check pnpm check ${{ matrix.target }}
- name: Tauri build - name: Tauri build
if: startsWith(matrix.os, 'windows') || startsWith(matrix.os,'macos') || startsWith(matrix.target,'x86_64')
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -66,7 +110,49 @@ jobs:
releaseDraft: false releaseDraft: false
prerelease: false prerelease: false
tauriScript: pnpm tauriScript: pnpm
args: -f default-meta args: -f default-meta --target ${{ matrix.target }}
- name: Tauri build (Ubuntu Arm64)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.target,'aarch64')
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++
PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig
PKG_CONFIG_ALLOW_CROSS: 1
with:
tagName: v__VERSION__
releaseName: "Clash Verge v__VERSION__"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: false
tauriScript: pnpm
args: -f default-meta --target ${{ matrix.target }} -b deb,updater
- name: Tauri build (Ubuntu Armv7)
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.target,'armv7')
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
CC_armv7_unknown_linux_gnueabihf: arm-linux-gnueabihf-gcc
CXX_armv7_unknown_linux_gnueabihf: arm-linux-gnueabihf-g++
PKG_CONFIG_PATH: /usr/lib/arm-linux-gnueabihf/pkgconfig
PKG_CONFIG_ALLOW_CROSS: 1
with:
tagName: v__VERSION__
releaseName: "Clash Verge v__VERSION__"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: false
tauriScript: pnpm
args: -f default-meta --target ${{ matrix.target }} -b deb,updater
- name: Portable Bundle - name: Portable Bundle
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
@@ -81,9 +167,7 @@ jobs:
release-update: release-update:
needs: [release] needs: [release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: startsWith(github.ref, 'refs/tags/v')
startsWith(github.repository, 'wonfen') &&
startsWith(github.ref, 'refs/tags/v')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -28,7 +28,6 @@ env:
jobs: jobs:
release: release:
runs-on: ${{ github.event.inputs.os }} runs-on: ${{ github.event.inputs.os }}
if: startsWith(github.repository, 'wonfen')
steps: steps:
- name: System Version - name: System Version
run: | run: |

View File

@@ -1,12 +1,10 @@
name: Updater CI name: Updater CI
on: workflow_dispatch on: workflow_dispatch
permissions: write-all
jobs: jobs:
release-update: release-update:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: |
startsWith(github.repository, 'wonfen')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3

View File

@@ -1,18 +1,18 @@
<h1 align="center"> <h1 align="center">
<img src="./src/assets/image/logo.png" alt="Clash" width="128" /> <img src="./src/assets/image/logo.png" alt="Clash" width="128" />
<br> <br>
Clash Verge Continuation of Clash Verge
<br> <br>
</h1> </h1>
<h3 align="center"> <h3 align="center">
A 接盘 Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">tauri</a>. A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">tauri</a>.
</h3> </h3>
## Features ## Features
- Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core. - Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core.
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/wonfen/clash-verge-rev/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97) - Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/clash-verge-rev/clash-verge-rev/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97)
- Simple UI and supports custom theme color. - Simple UI and supports custom theme color.
- Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core. - Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core.
- System proxy setting and guard. - System proxy setting and guard.
@@ -39,13 +39,13 @@ A 接盘 Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">t
## Install ## Install
Download from [release](https://github.com/wonfen/clash-verge-rev/releases). Supports Windows x64, Linux x86_64 and macOS 11+ Download from [release](https://github.com/clash-verge-rev/clash-verge-rev/releases). Supports Windows x64, Linux x86_64 and macOS 11+
- [Windows x64](https://github.com/wonfen/clash-verge-rev/releases/download/v1.4.0/Clash.Verge_1.4.0_x64_zh-CN.msi) - [Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.0/Clash.Verge_1.4.0_x64_zh-CN.msi)
- [macOS intel](https://github.com/wonfen/clash-verge-rev/releases/download/v1.4.0/Clash.Verge_1.4.0_x64.dmg) - [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.0/Clash.Verge_1.4.0_x64.dmg)
- [macOS arm](https://github.com/wonfen/clash-verge-rev/releases/download/v1.4.0/Clash.Verge_1.4.0_aarch64.dmg) - [macOS arm](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.0/Clash.Verge_1.4.0_aarch64.dmg)
- [Linux AppImage](https://github.com/wonfen/clash-verge-rev/releases/download/v1.4.0/clash-verge_1.4.0_amd64.AppImage) - [Linux AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.0/clash-verge_1.4.0_amd64.AppImage)
- [Linux deb](https://github.com/wonfen/clash-verge-rev/releases/download/v1.4.0/clash-verge_1.4.0_amd64.deb) - [Linux deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.0/clash-verge_1.4.0_amd64.deb)
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+ Or you can build it yourself. Supports Windows, Linux and macOS 10.15+

View File

@@ -1,3 +1,21 @@
## v1.4.2
### Features
- update clash meta core to mihomo 1.17.0
- fixed the problem of not being able to set the system proxy when there is a dial-up link on windows system [#833](https://github.com/zzzgydi/clash-verge/issues/833)
- support new clash field
- support random mixed port
- add windows x86 and linux armv7 support
- support disable tray click event
- add download progress for updater
- support drag to reorder the profile
- embed emoji fonts
- update depends
- improve UI style
---
## v1.4.1 ## v1.4.1
### Features ### Features

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 MiB

After

Width:  |  Height:  |  Size: 6.9 MiB

View File

@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "1.4.1", "version": "1.4.2",
"license": "GPL-3.0", "license": "GPL-3.0",
"scripts": { "scripts": {
"dev": "tauri dev -f default-meta", "dev": "tauri dev -f default-meta",
@@ -18,57 +18,60 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^5.14.14", "@mui/icons-material": "^5.14.19",
"@mui/lab": "5.0.0-alpha.149", "@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^5.14.14", "@mui/material": "^5.14.19",
"@mui/x-data-grid": "^6.16.3", "@mui/x-data-grid": "^6.18.2",
"@tauri-apps/api": "^1.3.0", "@tauri-apps/api": "^1.5.1",
"ahooks": "^3.7.2", "ahooks": "^3.7.8",
"axios": "^1.1.3", "axios": "^1.6.2",
"dayjs": "1.11.5", "dayjs": "1.11.5",
"i18next": "^22.0.4", "i18next": "^23.7.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"monaco-editor": "^0.34.1", "monaco-editor": "^0.34.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-hook-form": "^7.39.5", "react-hook-form": "^7.48.2",
"react-i18next": "^12.0.0", "react-i18next": "^13.5.0",
"react-router-dom": "^6.4.3", "react-router-dom": "^6.20.0",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"react-virtuoso": "^3.1.3", "react-virtuoso": "^4.6.2",
"recoil": "^0.7.6", "recoil": "^0.7.7",
"snarkdown": "^2.0.0", "snarkdown": "^2.0.0",
"tar": "^6.2.0", "swr": "^1.3.0",
"swr": "^1.3.0" "tar": "^6.2.0"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^5.0.3", "@actions/github": "^5.1.1",
"@tauri-apps/cli": "^1.3.1", "@tauri-apps/cli": "^1.5.6",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.14.180", "@types/lodash": "^4.14.202",
"@types/lodash-es": "^4.17.7", "@types/lodash-es": "^4.17.12",
"@types/react": "^18.2.37", "@types/react": "^18.2.39",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.2.17",
"@types/react-transition-group": "^4.4.9", "@types/react-transition-group": "^4.4.9",
"@vitejs/plugin-react": "^4.1.0", "@vitejs/plugin-react": "^4.2.0",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.10",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"fs-extra": "^10.0.0", "fs-extra": "^11.2.0",
"https-proxy-agent": "^5.0.1", "https-proxy-agent": "^5.0.1",
"husky": "^7.0.0", "husky": "^7.0.4",
"node-fetch": "^3.2.6", "node-fetch": "^3.3.2",
"prettier": "^2.7.1", "prettier": "^2.8.8",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"sass": "^1.54.0", "sass": "^1.69.5",
"typescript": "^4.7.4", "typescript": "^5.3.2",
"vite": "^4.5.0", "vite": "^4.5.0",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.1.0" "vite-plugin-svgr": "^4.2.0"
}, },
"prettier": { "prettier": {
"tabWidth": 2, "tabWidth": 2,

1163
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,122 +11,127 @@ const cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge"); const TEMP_DIR = path.join(cwd, "node_modules/.verge");
const FORCE = process.argv.includes("--force"); const FORCE = process.argv.includes("--force");
const SIDECAR_HOST = execSync("rustc -vV") const PLATFORM_MAP = {
.toString() "x86_64-pc-windows-msvc": "win32",
.match(/(?<=host: ).+(?=\s*)/g)[0]; "i686-pc-windows-msvc": "win32",
"aarch64-pc-windows-msvc": "win32",
/* ======= clash ======= "x86_64-apple-darwin": "darwin",
const CLASH_STORAGE_PREFIX = "https://release.dreamacro.workers.dev/"; "aarch64-apple-darwin": "darwin",
const CLASH_URL_PREFIX = "x86_64-unknown-linux-gnu": "linux",
"https://github.com/Dreamacro/clash/releases/download/premium/"; "aarch64-unknown-linux-gnu": "linux",
const CLASH_LATEST_DATE = "latest"; "armv7-unknown-linux-gnueabihf": "linux",
const CLASH_BACKUP_URL_PREFIX =
"https://github.com/zhongfly/Clash-premium-backup/releases/download/";
const CLASH_BACKUP_LATEST_DATE = "2023-09-05-gdcc8d87";
//https://github.com/zhongfly/Clash-premium-backup/releases/download/2023-09-05-gdcc8d87/clash-windows-amd64-2023-09-05-gdcc8d87.zip
//https://github.com/zhongfly/Clash-premium-backup/releases/download/2023-09-05-gdcc8d87/clash-windows-amd64-n2023-09-05-gdcc8d87.zip
const CLASH_MAP = {
"win32-x64": "clash-windows-amd64",
"darwin-x64": "clash-darwin-amd64",
"darwin-arm64": "clash-darwin-arm64",
"linux-x64": "clash-linux-amd64",
"linux-arm64": "clash-linux-arm64",
}; };
*/ const ARCH_MAP = {
/* ======= clash meta ======= */ "x86_64-pc-windows-msvc": "x64",
const META_URL_PREFIX = `https://github.com/wonfen/Clash.Meta/releases/download/latest`; "i686-pc-windows-msvc": "ia32",
// const META_VERSION = "2023.11.23"; "aarch64-pc-windows-msvc": "arm64",
"x86_64-apple-darwin": "x64",
"aarch64-apple-darwin": "arm64",
"x86_64-unknown-linux-gnu": "x64",
"aarch64-unknown-linux-gnu": "arm64",
"armv7-unknown-linux-gnueabihf": "arm",
};
const arg1 = process.argv.slice(2)[0];
const arg2 = process.argv.slice(2)[1];
const target = arg1 === "--force" ? arg2 : arg1;
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process;
const SIDECAR_HOST = target
? target
: execSync("rustc -vV")
.toString()
.match(/(?<=host: ).+(?=\s*)/g)[0];
/* ======= clash meta alpha======= */
const VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt";
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`;
let META_ALPHA_VERSION;
const META_ALPHA_MAP = {
"win32-x64": "mihomo-windows-amd64",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
};
// Fetch the latest release version from the version.txt file
async function getLatestVersion() {
try {
const response = await fetch(VERSION_URL, { method: "GET" });
let v = await response.text();
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
console.log(`Latest release version: ${META_ALPHA_VERSION}`);
} catch (error) {
console.error("Error fetching latest release version:", error.message);
process.exit(1);
}
}
/* ======= clash meta stable ======= */
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
let META_VERSION = "v1.17.0";
const META_MAP = { const META_MAP = {
"win32-x64": "clash.meta-win-amd64", "win32-x64": "mihomo-windows-amd64",
"darwin-x64": "clash.meta-darwin-amd64", "win32-ia32": "mihomo-windows-386",
"darwin-arm64": "clash.meta-darwin-arm64", "win32-arm64": "mihomo-windows-arm64",
"linux-x64": "clash.meta-linux-amd64", "darwin-x64": "mihomo-darwin-amd64",
"linux-arm64": "clash.meta-linux-arm64", "darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
}; };
/* /*
* check available * check available
*/ */
const { platform, arch } = process;
/*
if (!CLASH_MAP[`${platform}-${arch}`]) {
throw new Error(`clash unsupported platform "${platform}-${arch}"`);
}
*/
if (!META_MAP[`${platform}-${arch}`]) { if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`); throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
);
} }
/*
function clash() {
const name = CLASH_MAP[`${platform}-${arch}`];
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
);
}
/**
* core info
*/
function clashMetaAlpha() {
const name = META_ALPHA_MAP[`${platform}-${arch}`];
const isWin = platform === "win32"; const isWin = platform === "win32";
const urlExt = isWin ? "zip" : "gz"; const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${urlExt}`; const downloadURL = `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`; const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}.${urlExt}`; const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
return { return {
name: "clash", name: "clash-meta-alpha",
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`, targetFile: `clash-meta-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile, exeFile,
zipFile, zipFile,
downloadURL, downloadURL,
}; };
} }
function clashBackup() {
const name = CLASH_MAP[`${platform}-${arch}`];
const isWin = platform === "win32";
const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${CLASH_BACKUP_URL_PREFIX}${CLASH_BACKUP_LATEST_DATE}/${name}-n${CLASH_BACKUP_LATEST_DATE}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}.${urlExt}`;
return {
name: "clash",
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile,
zipFile,
downloadURL,
};
}
function clashS3() {
const name = CLASH_MAP[`${platform}-${arch}`];
const isWin = platform === "win32";
const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${CLASH_STORAGE_PREFIX}${CLASH_LATEST_DATE}/${name}-${CLASH_LATEST_DATE}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}.${urlExt}`;
return {
name: "clash",
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile,
zipFile,
downloadURL,
};
}
*/
function clashMeta() { function clashMeta() {
const name = META_MAP[`${platform}-${arch}`]; const name = META_MAP[`${platform}-${arch}`];
const isWin = platform === "win32"; const isWin = platform === "win32";
/* const urlExt = isWin ? "zip" : "gz"; const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${META_URL_PREFIX}${META_VERSION}/${name}-${META_VERSION}.${urlExt}`; const downloadURL = `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`; const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}-${META_VERSION}.${urlExt}`; */ const zipFile = `${name}-${META_VERSION}.${urlExt}`;
const urlExt = isWin ? "zip" : "tgz";
const downloadURL = `${META_URL_PREFIX}/${name}.${urlExt}`;
const exeFile = isWin ? "虚空终端-win-amd64.exe" : name;
const zipFile = `${name}.${urlExt}`;
return { return {
name: "clash-meta", name: "clash-meta",
@@ -136,7 +141,6 @@ function clashMeta() {
downloadURL, downloadURL,
}; };
} }
/** /**
* download sidecar and rename * download sidecar and rename
*/ */
@@ -218,58 +222,6 @@ async function resolveSidecar(binInfo) {
} }
} }
/**
* prepare clash core
* if the core version is not updated in time, use S3 storage as a backup.
*/
async function resolveClash() {
try {
return await resolveSidecar(clash());
} catch {
console.log(`[WARN]: clash core needs to be updated`);
return await resolveSidecar(clashS3());
}
}
/**
* only Windows
* get the wintun.dll (not required)
async function resolveWintun() {
const { platform } = process;
if (platform !== "win32") return;
const url = "https://www.wintun.net/builds/wintun-0.14.1.zip";
const tempDir = path.join(TEMP_DIR, "wintun");
const tempZip = path.join(tempDir, "wintun.zip");
const wintunPath = path.join(tempDir, "wintun/bin/amd64/wintun.dll");
const targetPath = path.join(cwd, "src-tauri/resources", "wintun.dll");
if (!FORCE && (await fs.pathExists(targetPath))) return;
await fs.mkdirp(tempDir);
if (!(await fs.pathExists(tempZip))) {
await downloadFile(url, tempZip);
}
// unzip
const zip = new AdmZip(tempZip);
zip.extractAllTo(tempDir, true);
if (!(await fs.pathExists(wintunPath))) {
throw new Error(`path not found "${wintunPath}"`);
}
await fs.rename(wintunPath, targetPath);
await fs.remove(tempDir);
console.log(`[INFO]: resolve wintun.dll finished`);
}
*/
/** /**
* download the file to the resources dir * download the file to the resources dir
*/ */
@@ -358,7 +310,16 @@ const resolveEnableLoopback = () =>
const tasks = [ const tasks = [
// { name: "clash", func: resolveClash, retry: 5 }, // { name: "clash", func: resolveClash, retry: 5 },
{ name: "clash-meta", func: () => resolveSidecar(clashMeta()), retry: 5 }, {
name: "clash-meta-alpha",
func: () => getLatestVersion().then(() => resolveSidecar(clashMetaAlpha())),
retry: 5,
},
{
name: "clash-meta",
func: () => resolveSidecar(clashMeta()),
retry: 5,
},
// { name: "wintun", func: resolveWintun, retry: 5, winOnly: true }, // { name: "wintun", func: resolveWintun, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5, winOnly: true }, { name: "service", func: resolveService, retry: 5, winOnly: true },
{ name: "install", func: resolveInstall, retry: 5, winOnly: true }, { name: "install", func: resolveInstall, retry: 5, winOnly: true },

View File

@@ -45,6 +45,7 @@ async function resolveUpdater() {
"darwin-intel": { signature: "", url: "" }, "darwin-intel": { signature: "", url: "" },
"darwin-x86_64": { signature: "", url: "" }, "darwin-x86_64": { signature: "", url: "" },
"linux-x86_64": { signature: "", url: "" }, "linux-x86_64": { signature: "", url: "" },
"linux-aarch64": { signature: "", url: "" },
"windows-x86_64": { signature: "", url: "" }, "windows-x86_64": { signature: "", url: "" },
}, },
}; };
@@ -53,17 +54,43 @@ async function resolveUpdater() {
const { name, browser_download_url } = asset; const { name, browser_download_url } = asset;
// win64 url // win64 url
if (name.endsWith(".msi.zip") && name.includes("en-US")) { if (
name.endsWith(".msi.zip") &&
name.includes("en-US") &&
name.includes("x64")
) {
updateData.platforms.win64.url = browser_download_url; updateData.platforms.win64.url = browser_download_url;
updateData.platforms["windows-x86_64"].url = browser_download_url; updateData.platforms["windows-x86_64"].url = browser_download_url;
} }
// win64 signature // win64 signature
if (name.endsWith(".msi.zip.sig") && name.includes("en-US")) { if (
name.endsWith(".msi.zip.sig") &&
name.includes("en-US") &&
name.includes("x64")
) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms.win64.signature = sig; updateData.platforms.win64.signature = sig;
updateData.platforms["windows-x86_64"].signature = sig; updateData.platforms["windows-x86_64"].signature = sig;
} }
// win32 url
if (
name.endsWith(".msi.zip") &&
name.includes("en-US") &&
name.includes("x86")
) {
updateData.platforms["windows-i686"].url = browser_download_url;
}
// win32 signature
if (
name.endsWith(".msi.zip.sig") &&
name.includes("en-US") &&
name.includes("x86")
) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-i686"].signature = sig;
}
// darwin url (intel) // darwin url (intel)
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) { if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
updateData.platforms.darwin.url = browser_download_url; updateData.platforms.darwin.url = browser_download_url;
@@ -92,12 +119,18 @@ async function resolveUpdater() {
if (name.endsWith(".AppImage.tar.gz")) { if (name.endsWith(".AppImage.tar.gz")) {
updateData.platforms.linux.url = browser_download_url; updateData.platforms.linux.url = browser_download_url;
updateData.platforms["linux-x86_64"].url = browser_download_url; updateData.platforms["linux-x86_64"].url = browser_download_url;
// 暂时使用x64版本的url和sig使得可以检查更新但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].url = browser_download_url;
// updateData.platforms["linux-armv7"].url = browser_download_url;
} }
// linux signature // linux signature
if (name.endsWith(".AppImage.tar.gz.sig")) { if (name.endsWith(".AppImage.tar.gz.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms.linux.signature = sig; updateData.platforms.linux.signature = sig;
updateData.platforms["linux-x86_64"].signature = sig; updateData.platforms["linux-x86_64"].signature = sig;
// 暂时使用x64版本的url和sig使得可以检查更新但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].signature = sig;
// updateData.platforms["linux-armv7"].signature = sig;
} }
}); });

515
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "1.4.1" version = "1.4.2"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi"] authors = ["zzzgydi"]
license = "GPL-3.0" license = "GPL-3.0"
repository = "https://github.com/wonfen/clash-verge-rev.git" repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge" default-run = "clash-verge"
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
@@ -14,40 +14,39 @@ tauri-build = { version = "1", features = [] }
[dependencies] [dependencies]
warp = "0.3" warp = "0.3"
which = "4.2.2" which = "5.0.0"
anyhow = "1.0" anyhow = "1.0"
dirs = "5.0.0" dirs = "5.0"
open = "4.0.1" open = "5.0"
log = "0.4.14" log = "0.4"
ctrlc = "3.2.3" ctrlc = "3.4"
dunce = "1.0.2" dunce = "1.0"
log4rs = "1.0.0" log4rs = "1"
nanoid = "0.4.0" nanoid = "0.4"
chrono = "0.4.19" chrono = "0.4"
sysinfo = "0.29" sysinfo = "0.29"
sysproxy = "0.3" sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
rquickjs = "0.1.7" rquickjs = "0.3"
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.9" serde_yaml = "0.9"
auto-launch = "0.5" auto-launch = "0.5"
once_cell = "1.14.0" once_cell = "1.18"
port_scanner = "0.1.5" port_scanner = "0.1.5"
delay_timer = "0.11.1" delay_timer = "0.11"
parking_lot = "0.12.0" parking_lot = "0.12"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"] } reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
tauri = { version = "1.2.4", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] } tauri = { version = "1.5", features = ["clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
window-vibrancy = { version = "0.3.0" } window-vibrancy = { version = "0.4.3" }
window-shadows = { version = "0.2.0" } window-shadows = { version = "0.2" }
wry = { version = "0.24.3" }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.0.0" runas = "=1.1.0"
deelevate = "0.2.0" deelevate = "0.2.0"
winreg = { version = "0.50", features = ["transactions"] } winreg = { version = "0.52", features = ["transactions"] }
windows-sys = { version = "0.48", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] } windows-sys = { version = "0.52", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
[target.'cfg(windows)'.dependencies.tauri] [target.'cfg(windows)'.dependencies.tauri]
features = ["global-shortcut-all", "icon-png", "process-all", "shell-all", "system-tray", "updater", "window-all"] features = ["global-shortcut-all", "icon-png", "process-all", "shell-all", "system-tray", "updater", "window-all"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -30,6 +30,11 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
wrap_err!(Config::profiles().data().append_item(item)) wrap_err!(Config::profiles().data().append_item(item))
} }
#[tauri::command]
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
}
#[tauri::command] #[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult { pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
let item = wrap_err!(PrfItem::from(item, file_data).await)?; let item = wrap_err!(PrfItem::from(item, file_data).await)?;
@@ -229,7 +234,6 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
wrap_err!(open::that(url)) wrap_err!(open::that(url))
} }
#[cfg(windows)] #[cfg(windows)]
pub mod uwp { pub mod uwp {
use super::*; use super::*;
@@ -299,4 +303,4 @@ pub mod uwp {
pub async fn invoke_uwp_tool() -> CmdResult { pub async fn invoke_uwp_tool() -> CmdResult {
Ok(()) Ok(())
} }
} }

View File

@@ -194,7 +194,10 @@ impl PrfItem {
// 使用软件自己的代理 // 使用软件自己的代理
if self_proxy { if self_proxy {
let port = Config::clash().data().get_mixed_port(); let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let proxy_scheme = format!("http://127.0.0.1:{port}"); let proxy_scheme = format!("http://127.0.0.1:{port}");

View File

@@ -55,7 +55,12 @@ impl IProfiles {
pub fn template() -> Self { pub fn template() -> Self {
Self { Self {
valid: Some(vec!["dns".into(), "sub-rules".into(), "unified-delay".into(), "tcp-concurrent".into()]), valid: Some(vec![
"dns".into(),
"sub-rules".into(),
"unified-delay".into(),
"tcp-concurrent".into(),
]),
items: Some(vec![]), items: Some(vec![]),
..Self::default() ..Self::default()
} }
@@ -151,6 +156,30 @@ impl IProfiles {
self.save_file() self.save_file()
} }
/// reorder items
pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
let mut items = self.items.take().unwrap_or(vec![]);
let mut old_index = None;
let mut new_index = None;
for i in 0..items.len() {
if items[i].uid == Some(active_id.clone()) {
old_index = Some(i);
}
if items[i].uid == Some(over_id.clone()) {
new_index = Some(i);
}
}
if old_index.is_none() || new_index.is_none() {
return Ok(());
}
let item = items.remove(old_index.unwrap());
items.insert(new_index.unwrap(), item);
self.items = Some(items);
self.save_file()
}
/// update the item value /// update the item value
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
let mut items = self.items.take().unwrap_or(vec![]); let mut items = self.items.take().unwrap_or(vec![]);

View File

@@ -93,6 +93,12 @@ pub struct IVerge {
/// window size and position /// window size and position
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub window_size_position: Option<Vec<f64>>, pub window_size_position: Option<Vec<f64>>,
/// 是否启用随机端口
pub enable_random_port: Option<bool>,
/// verge mixed port 用于覆盖 clash 的 mixed port
pub verge_mixed_port: Option<u16>,
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -139,6 +145,8 @@ impl IVerge {
enable_auto_launch: Some(false), enable_auto_launch: Some(false),
enable_silent_start: Some(false), enable_silent_start: Some(false),
enable_system_proxy: Some(false), enable_system_proxy: Some(false),
enable_random_port: Some(false),
verge_mixed_port: Some(7890),
enable_proxy_guard: Some(false), enable_proxy_guard: Some(false),
proxy_guard_duration: Some(30), proxy_guard_duration: Some(30),
auto_close_connection: Some(true), auto_close_connection: Some(true),
@@ -177,6 +185,8 @@ impl IVerge {
patch!(enable_service_mode); patch!(enable_service_mode);
patch!(enable_auto_launch); patch!(enable_auto_launch);
patch!(enable_silent_start); patch!(enable_silent_start);
patch!(enable_random_port);
patch!(verge_mixed_port);
patch!(enable_system_proxy); patch!(enable_system_proxy);
patch!(enable_proxy_guard); patch!(enable_proxy_guard);
patch!(system_proxy_bypass); patch!(system_proxy_bypass);

View File

@@ -147,6 +147,7 @@ impl CoreManager {
// fix #212 // fix #212
let args = match clash_core.as_str() { let args = match clash_core.as_str() {
"clash-meta" => vec!["-m", "-d", app_dir, "-f", config_path], "clash-meta" => vec!["-m", "-d", app_dir, "-f", config_path],
"clash-meta-alpha" => vec!["-m", "-d", app_dir, "-f", config_path],
_ => vec!["-d", app_dir, "-f", config_path], _ => vec!["-d", app_dir, "-f", config_path],
}; };
@@ -258,8 +259,9 @@ impl CoreManager {
/// 切换核心 /// 切换核心
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> { pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?; let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
const CLASH_CORES: [&str; 3] = ["clash", "clash-meta", "clash-meta-alpha"];
if &clash_core != "clash" && &clash_core != "clash-meta" { if !CLASH_CORES.contains(&clash_core.as_str()) {
bail!("invalid clash core name \"{clash_core}\""); bail!("invalid clash core name \"{clash_core}\"");
} }

View File

@@ -4,7 +4,6 @@ use once_cell::sync::OnceCell;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use tauri::{AppHandle, GlobalShortcutManager}; use tauri::{AppHandle, GlobalShortcutManager};
use wry::application::accelerator::Accelerator;
pub struct Hotkey { pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置 current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
@@ -35,7 +34,7 @@ impl Hotkey {
match (key, func) { match (key, func) {
(Some(key), Some(func)) => { (Some(key), Some(func)) => {
log_err!(Self::check_key(key).and_then(|_| self.register(key, func))); log_err!(self.register(key, func));
} }
_ => { _ => {
let key = key.unwrap_or("None"); let key = key.unwrap_or("None");
@@ -50,16 +49,6 @@ impl Hotkey {
Ok(()) Ok(())
} }
/// 检查一个键是否合法
fn check_key(hotkey: &str) -> Result<()> {
// fix #287
// tauri的这几个方法全部有Result expect会panic先检测一遍避免挂了
if hotkey.parse::<Accelerator>().is_err() {
bail!("invalid hotkey `{hotkey}`");
}
Ok(())
}
fn get_manager(&self) -> Result<impl GlobalShortcutManager> { fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
let app_handle = self.app_handle.lock(); let app_handle = self.app_handle.lock();
if app_handle.is_none() { if app_handle.is_none() {
@@ -109,11 +98,6 @@ impl Hotkey {
let (del, add) = Self::get_diff(old_map, new_map); let (del, add) = Self::get_diff(old_map, new_map);
// 先检查一遍所有新的热键是不是可以用的
for (hotkey, _) in add.iter() {
Self::check_key(hotkey)?;
}
del.iter().for_each(|key| { del.iter().for_each(|key| {
let _ = self.unregister(key); let _ = self.unregister(key);
}); });

View File

@@ -27,7 +27,8 @@ static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;<local>";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1"; static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1";
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>"; static DEFAULT_BYPASS: &str =
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
impl Sysopt { impl Sysopt {
pub fn global() -> &'static Sysopt { pub fn global() -> &'static Sysopt {
@@ -43,7 +44,10 @@ impl Sysopt {
/// init the sysproxy /// init the sysproxy
pub fn init_sysproxy(&self) -> Result<()> { pub fn init_sysproxy(&self) -> Result<()> {
let port = { Config::clash().latest().get_mixed_port() }; let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let (enable, bypass) = { let (enable, bypass) = {
let verge = Config::verge(); let verge = Config::verge();
@@ -284,7 +288,12 @@ impl Sysopt {
log::debug!(target: "app", "try to guard the system proxy"); log::debug!(target: "app", "try to guard the system proxy");
let port = { Config::clash().latest().get_mixed_port() }; let port = {
Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port())
};
let sysproxy = Sysproxy { let sysproxy = Sysproxy {
enable: true, enable: true,

View File

@@ -145,12 +145,16 @@ impl Tray {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
let indication_icon = if *system_proxy { let mut indication_icon = if *system_proxy {
include_bytes!("../../icons/win-tray-icon-activated.png").to_vec() include_bytes!("../../icons/win-tray-icon-activated.png").to_vec()
} else { } else {
include_bytes!("../../icons/win-tray-icon.png").to_vec() include_bytes!("../../icons/win-tray-icon.png").to_vec()
}; };
if *tun_mode {
indication_icon = include_bytes!("../../icons/win-tray-icon-tun.png").to_vec();
}
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon)); let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
} }
@@ -182,7 +186,8 @@ impl Tray {
match tray_event.as_str() { match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(), "system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(), "tun_mode" => feat::toggle_tun_mode(),
_ => resolve::create_window(app_handle), "main_window" => resolve::create_window(app_handle),
_ => {}
} }
} }
@@ -199,11 +204,11 @@ impl Tray {
"open_window" => resolve::create_window(app_handle), "open_window" => resolve::create_window(app_handle),
"system_proxy" => feat::toggle_system_proxy(), "system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(), "tun_mode" => feat::toggle_tun_mode(),
"copy_env_sh" => feat::copy_clash_env("sh"), "copy_env_sh" => feat::copy_clash_env(app_handle, "sh"),
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
"copy_env_cmd" => feat::copy_clash_env("cmd"), "copy_env_cmd" => feat::copy_clash_env(app_handle, "cmd"),
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
"copy_env_ps" => feat::copy_clash_env("ps"), "copy_env_ps" => feat::copy_clash_env(app_handle, "ps"),
"open_app_dir" => crate::log_err!(cmds::open_app_dir()), "open_app_dir" => crate::log_err!(cmds::open_app_dir()),
"open_core_dir" => crate::log_err!(cmds::open_core_dir()), "open_core_dir" => crate::log_err!(cmds::open_core_dir()),
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()), "open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),

View File

@@ -21,6 +21,7 @@ pub enum ChainType {
pub enum ChainSupport { pub enum ChainSupport {
Clash, Clash,
ClashMeta, ClashMeta,
ClashMetaAlpha,
All, All,
} }
@@ -60,9 +61,19 @@ impl ChainItem {
let hy_alpn = let hy_alpn =
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js")); ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
// meta 的一些处理
let meta_guard_alpha =
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组
let hy_alpn_alpha =
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
vec![ vec![
(ChainSupport::ClashMeta, hy_alpn), (ChainSupport::ClashMeta, hy_alpn),
(ChainSupport::ClashMeta, meta_guard), (ChainSupport::ClashMeta, meta_guard),
(ChainSupport::ClashMetaAlpha, hy_alpn_alpha),
(ChainSupport::ClashMetaAlpha, meta_guard_alpha),
] ]
} }
@@ -81,6 +92,7 @@ impl ChainSupport {
(ChainSupport::All, _) => true, (ChainSupport::All, _) => true,
(ChainSupport::Clash, "clash") => true, (ChainSupport::Clash, "clash") => true,
(ChainSupport::ClashMeta, "clash-meta") => true, (ChainSupport::ClashMeta, "clash-meta") => true,
(ChainSupport::ClashMetaAlpha, "clash-meta-alpha") => true,
_ => false, _ => false,
}, },
None => true, None => true,

View File

@@ -21,7 +21,7 @@ pub const DEFAULT_FIELDS: [&str; 5] = [
"rule-providers", "rule-providers",
]; ];
pub const OTHERS_FIELDS: [&str; 30] = [ pub const OTHERS_FIELDS: [&str; 31] = [
"dns", "dns",
"tun", "tun",
"ebpf", "ebpf",
@@ -50,6 +50,7 @@ pub const OTHERS_FIELDS: [&str; 30] = [
"tcp-concurrent", // meta "tcp-concurrent", // meta
"enable-process", // meta "enable-process", // meta
"find-process-mode", // meta "find-process-mode", // meta
"skip-auth-prefixes", // meta
"external-controller-tls", // meta "external-controller-tls", // meta
"global-client-fingerprint", // meta "global-client-fingerprint", // meta
]; ];

View File

@@ -3,7 +3,7 @@ use anyhow::Result;
use serde_yaml::Mapping; use serde_yaml::Mapping;
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> { pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
use rquickjs::{Context, Func, Runtime}; use rquickjs::{function::Func, Context, Runtime};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
let runtime = Runtime::new().unwrap(); let runtime = Runtime::new().unwrap();

View File

@@ -10,7 +10,7 @@ use crate::log_err;
use crate::utils::resolve; use crate::utils::resolve;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use wry::application::clipboard::Clipboard; use tauri::{AppHandle, ClipboardManager};
// 打开面板 // 打开面板
pub fn open_dashboard() { pub fn open_dashboard() {
@@ -162,8 +162,13 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
match { match {
let mixed_port = patch.get("mixed-port"); let mixed_port = patch.get("mixed-port");
if mixed_port.is_some() { let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
let changed = mixed_port != Config::clash().data().0.get("mixed-port"); if mixed_port.is_some() && !enable_random_port {
let changed = mixed_port.clone().unwrap()
!= Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
// 检查端口占用 // 检查端口占用
if changed { if changed {
if let Some(port) = mixed_port.clone().unwrap().as_u64() { if let Some(port) = mixed_port.clone().unwrap().as_u64() {
@@ -332,21 +337,21 @@ async fn update_core_config() -> Result<()> {
} }
/// copy env variable /// copy env variable
pub fn copy_clash_env(option: &str) { pub fn copy_clash_env(app_handle: &AppHandle, option: &str) {
let port = { Config::clash().data().get_client_info().port }; let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7890) };
let http_proxy = format!("http://127.0.0.1:{}", port); let http_proxy = format!("http://127.0.0.1:{}", port);
let socks5_proxy = format!("socks5://127.0.0.1:{}", port); let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
let sh = format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}"); let sh =
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
let cmd: String = format!("set http_proxy={http_proxy} \n set https_proxy={http_proxy}"); let cmd: String = format!("set http_proxy={http_proxy} \n set https_proxy={http_proxy}");
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\""); let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
let mut cliboard = app_handle.clipboard_manager();
let mut cliboard = Clipboard::new();
match option { match option {
"sh" => cliboard.write_text(sh), "sh" => cliboard.write_text(sh).unwrap_or_default(),
"cmd" => cliboard.write_text(cmd), "cmd" => cliboard.write_text(cmd).unwrap_or_default(),
"ps" => cliboard.write_text(ps), "ps" => cliboard.write_text(ps).unwrap_or_default(),
_ => log::error!(target: "app", "copy_clash_env: Invalid option! {option}"), _ => log::error!(target: "app", "copy_clash_env: Invalid option! {option}"),
} };
} }

View File

@@ -59,6 +59,7 @@ fn main() -> std::io::Result<()> {
cmds::patch_profile, cmds::patch_profile,
cmds::create_profile, cmds::create_profile,
cmds::import_profile, cmds::import_profile,
cmds::reorder_profile,
cmds::update_profile, cmds::update_profile,
cmds::delete_profile, cmds::delete_profile,
cmds::read_profile_file, cmds::read_profile_file,

View File

@@ -1,8 +1,28 @@
use crate::config::IVerge;
use crate::{config::Config, core::*, utils::init, utils::server}; use crate::{config::Config, core::*, utils::init, utils::server};
use crate::{log_err, trace_err}; use crate::{log_err, trace_err};
use anyhow::Result; use anyhow::Result;
use serde_yaml::Mapping;
use std::net::TcpListener;
use tauri::{App, AppHandle, Manager}; use tauri::{App, AppHandle, Manager};
pub fn find_unused_port() -> Result<u16> {
match TcpListener::bind("127.0.0.1:0") {
Ok(listener) => {
let port = listener.local_addr()?.port();
Ok(port)
}
Err(_) => {
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
log::warn!(target: "app", "use default port: {}", port);
Ok(port)
}
}
}
/// handle something when start app /// handle something when start app
pub fn resolve_setup(app: &mut App) { pub fn resolve_setup(app: &mut App) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -12,6 +32,33 @@ pub fn resolve_setup(app: &mut App) {
log_err!(init::init_resources(app.package_info())); log_err!(init::init_resources(app.package_info()));
// 处理随机端口
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
let mut port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
if enable_random_port {
port = find_unused_port().unwrap_or(
Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port()),
);
}
Config::verge().data().patch_config(IVerge {
verge_mixed_port: Some(port),
..IVerge::default()
});
let _ = Config::verge().data().save_file();
let mut mapping = Mapping::new();
mapping.insert("mixed-port".into(), port.into());
Config::clash().data().patch_config(mapping);
let _ = Config::clash().data().save_config();
// 启动核心 // 启动核心
log::trace!("init config"); log::trace!("init config");
log_err!(Config::init_config()); log_err!(Config::init_config());

View File

@@ -1,7 +1,7 @@
{ {
"package": { "package": {
"productName": "Clash Verge", "productName": "Clash Verge",
"version": "1.4.1" "version": "1.4.2"
}, },
"build": { "build": {
"distDir": "../dist", "distDir": "../dist",
@@ -26,7 +26,7 @@
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": ["resources"], "resources": ["resources"],
"externalBin": ["sidecar/clash-meta"], "externalBin": ["sidecar/clash-meta", "sidecar/clash-meta-alpha"],
"copyright": "© 2022 zzzgydi All Rights Reserved", "copyright": "© 2022 zzzgydi All Rights Reserved",
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "A Clash GUI based on tauri.", "shortDescription": "A Clash GUI based on tauri.",
@@ -53,8 +53,8 @@
"updater": { "updater": {
"active": true, "active": true,
"endpoints": [ "endpoints": [
"https://mirror.ghproxy.com/https://github.com/wonfen/clash-verge-rev/releases/download/updater/update-proxy.json", "https://mirror.ghproxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/wonfen/clash-verge-rev/releases/download/updater/update.json" "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
], ],
"dialog": false, "dialog": false,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
@@ -71,6 +71,9 @@
}, },
"globalShortcut": { "globalShortcut": {
"all": true "all": true
},
"clipboard": {
"all": true
} }
}, },
"windows": [], "windows": [],

Binary file not shown.

View File

@@ -1,21 +1,21 @@
.page-enter { .page-enter {
opacity: 0; opacity: 0;
transform: scale(0.9); clip-path: inset(0 100% 0 0); /* 完全隐藏内容 */
} }
.page-enter-active { .page-enter-active {
opacity: 1; opacity: 1;
transform: scale(1); clip-path: inset(0 0 0 0); /* 逐渐显示整个内容 */
transition: opacity 300ms, transform 300ms; transition: opacity 300ms, clip-path 300ms ease-in-out;
} }
.page-exit { .page-exit {
opacity: 1; opacity: 1;
transform: scale(0); clip-path: inset(0 0 0 0); /* 完全显示内容 */
} }
.page-exit-active { .page-exit-active {
opacity: 0; opacity: 0;
transform: scale(0.9); clip-path: inset(0 100% 0 0); /* 逐渐隐藏内容 */
transition: opacity 300ms, transform 300ms; transition: opacity 300ms, clip-path 300ms ease-in-out;
} }

View File

@@ -0,0 +1,4 @@
@font-face {
font-family: "twemoji mozilla";
src: url("../fonts/Twemoji.Mozilla.ttf");
}

View File

@@ -18,7 +18,7 @@ body {
--scroller-color: #90939980; --scroller-color: #90939980;
--background-color: #ffffff; --background-color: #ffffff;
--background-color-alpha: rgba(24, 103, 192, 0.1); --background-color-alpha: rgba(24, 103, 192, 0.1);
--border-radius: 12px; --border-radius: 8px;
} }
::selection { ::selection {
@@ -43,6 +43,7 @@ body {
@import "./layout.scss"; @import "./layout.scss";
@import "./page.scss"; @import "./page.scss";
@import "./anime.scss"; @import "./anime.scss";
@import "./font.scss";
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {

View File

@@ -57,7 +57,7 @@
.the-menu { .the-menu {
flex: 1 1 80%; flex: 1 1 80%;
overflow-y: auto; overflow-y: auto;
margin-bottom: 8px; margin-bottom: 0px;
} }
.the-traffic { .the-traffic {

View File

@@ -30,7 +30,6 @@
padding: 5px 5px; padding: 5px 5px;
box-sizing: border-box; box-sizing: border-box;
scrollbar-gutter: stable; scrollbar-gutter: stable;
background-color: var(--background-color);
.base-content { .base-content {
width: 100%; width: 100%;

View File

@@ -1,6 +1,7 @@
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
import { Typography } from "@mui/material"; import { Typography, alpha } from "@mui/material";
import { BaseErrorBoundary } from "./base-error-boundary"; import { BaseErrorBoundary } from "./base-error-boundary";
import { useCustomTheme } from "@/components/layout/use-custom-theme";
interface Props { interface Props {
title?: React.ReactNode; // the page title title?: React.ReactNode; // the page title
@@ -11,6 +12,9 @@ interface Props {
export const BasePage: React.FC<Props> = (props) => { export const BasePage: React.FC<Props> = (props) => {
const { title, header, contentStyle, children } = props; const { title, header, contentStyle, children } = props;
const { theme } = useCustomTheme();
const isDark = theme.palette.mode === "dark";
return ( return (
<BaseErrorBoundary> <BaseErrorBoundary>
@@ -23,8 +27,17 @@ export const BasePage: React.FC<Props> = (props) => {
{header} {header}
</header> </header>
<div className="base-container"> <div
<section> className="base-container"
style={{ backgroundColor: isDark ? "#090909" : "#ffffff" }}
>
<section
style={{
backgroundColor: isDark
? alpha(theme.palette.primary.main, 0.1)
: "",
}}
>
<div className="base-content" style={contentStyle} data-windrag> <div className="base-content" style={contentStyle} data-windrag>
{children} {children}
</div> </div>

View File

@@ -10,24 +10,22 @@ export const LayoutItem = (props: LinkProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<ListItem <ListItem sx={{ py: 0.5, maxWidth: 250, mx: "auto", padding: "1px 0px" }}>
sx={{ py: 0.5, maxWidth: 250, mx: "auto", padding: "4px 0px 4px 2px" }}
>
<ListItemButton <ListItemButton
selected={!!match} selected={!!match}
sx={[ sx={[
{ {
borderTopLeftRadius: 18, borderRadius: 3,
borderBottomLeftRadius: 18, marginLeft: 1,
marginRight: 1,
textAlign: "center", textAlign: "center",
"& .MuiListItemText-primary": { color: "text.secondary" }, "& .MuiListItemText-primary": { color: "text.secondary" },
}, },
({ palette: { mode, primary } }) => { ({ palette: { mode, primary } }) => {
/* const bgcolor = const bgcolor =
mode === "light" mode === "light"
? alpha(primary.main, 0.15) ? alpha(primary.main, 0.15)
: alpha(primary.main, 0.35); */ : alpha(primary.main, 0.35);
const bgcolor = mode === "light" ? "#ffffff" : "#0E1621";
const color = mode === "light" ? primary.main : primary.light; const color = mode === "light" ? primary.main : primary.light;
return { return {

View File

@@ -55,6 +55,9 @@ export const useCustomTheme = () => {
primary: setting.primary_text || dt.primary_text, primary: setting.primary_text || dt.primary_text,
secondary: setting.secondary_text || dt.secondary_text, secondary: setting.secondary_text || dt.secondary_text,
}, },
background: {
paper: dt.background_color,
},
}, },
typography: { typography: {
// todo // todo
@@ -84,7 +87,7 @@ export const useCustomTheme = () => {
} }
// css // css
const backgroundColor = mode === "light" ? "#ffffff" : "#0E1621"; const backgroundColor = mode === "light" ? "#ffffff" : "#0B121C";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5"; const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#54545480"; const scrollColor = mode === "light" ? "#90939980" : "#54545480";

View File

@@ -4,6 +4,8 @@ import { useEffect, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { import {
Box, Box,
Typography, Typography,
@@ -14,7 +16,7 @@ import {
Menu, Menu,
CircularProgress, CircularProgress,
} from "@mui/material"; } from "@mui/material";
import { RefreshRounded } from "@mui/icons-material"; import { RefreshRounded, DragIndicator } from "@mui/icons-material";
import { atomLoadingCache } from "@/services/states"; import { atomLoadingCache } from "@/services/states";
import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds"; import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
@@ -28,6 +30,7 @@ const round = keyframes`
`; `;
interface Props { interface Props {
id: string;
selected: boolean; selected: boolean;
activating: boolean; activating: boolean;
itemData: IProfileItem; itemData: IProfileItem;
@@ -37,6 +40,8 @@ interface Props {
export const ProfileItem = (props: Props) => { export const ProfileItem = (props: Props) => {
const { selected, activating, itemData, onSelect, onEdit } = props; const { selected, activating, itemData, onSelect, onEdit } = props;
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props.id });
const { t } = useTranslation(); const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<any>(null);
@@ -183,7 +188,12 @@ export const ProfileItem = (props: Props) => {
}; };
return ( return (
<> <Box
sx={{
transform: CSS.Transform.toString(transform),
transition,
}}
>
<ProfileBox <ProfileBox
aria-selected={selected} aria-selected={selected}
onClick={() => onSelect(false)} onClick={() => onSelect(false)}
@@ -212,17 +222,27 @@ export const ProfileItem = (props: Props) => {
<CircularProgress size={20} /> <CircularProgress size={20} />
</Box> </Box>
)} )}
<Box position="relative"> <Box position="relative">
<Typography <Box sx={{ display: "flex", justifyContent: "start" }}>
width="calc(100% - 36px)" <Box
variant="h6" ref={setNodeRef}
component="h2" sx={{ display: "flex", margin: "auto 0" }}
noWrap {...attributes}
title={name} {...listeners}
> >
{name} <DragIndicator sx={{ cursor: "grab" }} />
</Typography> </Box>
<Typography
width="calc(100% - 36px)"
variant="h6"
component="h2"
noWrap
title={name}
>
{name}
</Typography>
</Box>
{/* only if has url can it be updated */} {/* only if has url can it be updated */}
{hasUrl && ( {hasUrl && (
@@ -246,7 +266,6 @@ export const ProfileItem = (props: Props) => {
</IconButton> </IconButton>
)} )}
</Box> </Box>
{/* the second line show url's info or description */} {/* the second line show url's info or description */}
<Box sx={boxStyle}> <Box sx={boxStyle}>
{hasUrl ? ( {hasUrl ? (
@@ -271,7 +290,6 @@ export const ProfileItem = (props: Props) => {
</Typography> </Typography>
)} )}
</Box> </Box>
{/* the third line show extra info or last updated time */} {/* the third line show extra info or last updated time */}
{hasExtra ? ( {hasExtra ? (
<Box sx={{ ...boxStyle, fontSize: 14 }}> <Box sx={{ ...boxStyle, fontSize: 14 }}>
@@ -285,7 +303,6 @@ export const ProfileItem = (props: Props) => {
<span title="Updated Time">{parseExpire(updated)}</span> <span title="Updated Time">{parseExpire(updated)}</span>
</Box> </Box>
)} )}
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={progress} value={progress}
@@ -324,7 +341,7 @@ export const ProfileItem = (props: Props) => {
mode="yaml" mode="yaml"
onClose={() => setFileOpen(false)} onClose={() => setFileOpen(false)}
/> />
</> </Box>
); );
}; };

View File

@@ -8,7 +8,7 @@ import { Lock } from "@mui/icons-material";
import { import {
Box, Box,
Button, Button,
IconButton, Tooltip,
List, List,
ListItemButton, ListItemButton,
ListItemText, ListItemText,
@@ -18,11 +18,10 @@ import { closeAllConnections } from "@/services/api";
import { grantPermission } from "@/services/cmds"; import { grantPermission } from "@/services/cmds";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
/* const VALID_CORE = [ const VALID_CORE = [
{ name: "Clash", core: "clash" },
{ name: "Clash Meta", core: "clash-meta" }, { name: "Clash Meta", core: "clash-meta" },
]; */ { name: "Clash Meta Alpha", core: "clash-meta-alpha" },
const VALID_CORE = [{ name: "Clash Meta", core: "clash-meta" }]; ];
const OS = getSystem(); const OS = getSystem();
@@ -38,7 +37,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
close: () => setOpen(false), close: () => setOpen(false),
})); }));
const { clash_core = "clash" } = verge ?? {}; const { clash_core = "clash-meta" } = verge ?? {};
const onCoreChange = useLockFn(async (core: string) => { const onCoreChange = useLockFn(async (core: string) => {
if (core === clash_core) return; if (core === clash_core) return;
@@ -92,7 +91,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
contentSx={{ contentSx={{
pb: 0, pb: 0,
width: 320, width: 320,
height: 90, height: 180,
overflowY: "auto", overflowY: "auto",
userSelect: "text", userSelect: "text",
marginTop: "-8px", marginTop: "-8px",
@@ -112,30 +111,19 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
<ListItemText primary={each.name} secondary={`/${each.core}`} /> <ListItemText primary={each.name} secondary={`/${each.core}`} />
{(OS === "macos" || OS === "linux") && ( {(OS === "macos" || OS === "linux") && (
/* <IconButton <Tooltip title={t("Tun mode requires")}>
color="inherit" <Button
size="small" variant="outlined"
edge="end" size="small"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
onGrant(each.core); onGrant(each.core);
}} }}
> >
<Lock fontSize="inherit" /> {t("Grant")}
</IconButton> */ </Button>
<Button </Tooltip>
variant="outlined"
size="small"
title={t("Tun mode requires")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onGrant(each.core);
}}
>
{t("Grant")}
</Button>
)} )}
</ListItemButton> </ListItemButton>
))} ))}

View File

@@ -4,30 +4,35 @@ import { useLockFn } from "ahooks";
import { List, ListItem, ListItemText, TextField } from "@mui/material"; import { List, ListItem, ListItemText, TextField } from "@mui/material";
import { useClashInfo } from "@/hooks/use-clash"; import { useClashInfo } from "@/hooks/use-clash";
import { BaseDialog, DialogRef, Notice } from "@/components/base"; import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { useVerge } from "@/hooks/use-verge";
export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => { export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { clashInfo, patchInfo } = useClashInfo(); const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [port, setPort] = useState(clashInfo?.port ?? 7890); const [port, setPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.port ?? 7890
);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
if (clashInfo?.port) setPort(clashInfo?.port); if (verge?.verge_mixed_port) setPort(verge?.verge_mixed_port);
setOpen(true); setOpen(true);
}, },
close: () => setOpen(false), close: () => setOpen(false),
})); }));
const onSave = useLockFn(async () => { const onSave = useLockFn(async () => {
if (port === clashInfo?.port) { if (port === verge?.verge_mixed_port) {
setOpen(false); setOpen(false);
return; return;
} }
try { try {
await patchInfo({ "mixed-port": port }); await patchInfo({ "mixed-port": port });
await patchVerge({ verge_mixed_port: port });
setOpen(false); setOpen(false);
Notice.success("Change Clash port successfully!", 1000); Notice.success("Change Clash port successfully!", 1000);
} catch (err: any) { } catch (err: any) {

View File

@@ -2,17 +2,19 @@ import useSWR from "swr";
import snarkdown from "snarkdown"; import snarkdown from "snarkdown";
import { forwardRef, useImperativeHandle, useState, useMemo } from "react"; import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Box, styled } from "@mui/material"; import { Box, LinearProgress, styled } from "@mui/material";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { relaunch } from "@tauri-apps/api/process"; import { relaunch } from "@tauri-apps/api/process";
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater"; import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
import { BaseDialog, DialogRef, Notice } from "@/components/base"; import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { atomUpdateState } from "@/services/states"; import { atomUpdateState } from "@/services/states";
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
const UpdateLog = styled(Box)(() => ({ const UpdateLog = styled(Box)(() => ({
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" }, "h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
})); }));
let eventListener: UnlistenFn | null = null;
export const UpdateViewer = forwardRef<DialogRef>((props, ref) => { export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -26,6 +28,10 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
focusThrottleInterval: 36e5, // 1 hour focusThrottleInterval: 36e5, // 1 hour
}); });
const [downloaded, setDownloaded] = useState(0);
const [buffer, setBuffer] = useState(0);
const [total, setTotal] = useState(0);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => setOpen(true), open: () => setOpen(true),
close: () => setOpen(false), close: () => setOpen(false),
@@ -42,7 +48,19 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
const onUpdate = useLockFn(async () => { const onUpdate = useLockFn(async () => {
if (updateState) return; if (updateState) return;
setUpdateState(true); setUpdateState(true);
if (eventListener !== null) {
eventListener();
}
eventListener = await listen(
"tauri://update-download-progress",
(e: Event<any>) => {
setTotal(e.payload.contentLength);
setBuffer(e.payload.chunkLength);
setDownloaded((a) => {
return a + e.payload.chunkLength;
});
}
);
try { try {
await installUpdate(); await installUpdate();
await relaunch(); await relaunch();
@@ -65,6 +83,14 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
onOk={onUpdate} onOk={onUpdate}
> >
<UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} /> <UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} />
{updateState && (
<LinearProgress
variant="buffer"
value={(downloaded / total) * 100}
valueBuffer={buffer}
sx={{ marginTop: "5px" }}
/>
)}
</BaseDialog> </BaseDialog>
); );
}); });

View File

@@ -7,9 +7,10 @@ import {
MenuItem, MenuItem,
Typography, Typography,
IconButton, IconButton,
Tooltip,
} from "@mui/material"; } from "@mui/material";
import { ArrowForward, Settings } from "@mui/icons-material"; import { ArrowForward, Settings, Shuffle } from "@mui/icons-material";
import { DialogRef } from "@/components/base"; import { DialogRef, Notice } from "@/components/base";
import { useClash } from "@/hooks/use-clash"; import { useClash } from "@/hooks/use-clash";
import { GuardState } from "./mods/guard-state"; import { GuardState } from "./mods/guard-state";
import { WebUIViewer } from "./mods/web-ui-viewer"; import { WebUIViewer } from "./mods/web-ui-viewer";
@@ -20,6 +21,7 @@ import { SettingList, SettingItem } from "./mods/setting-comp";
import { ClashCoreViewer } from "./mods/clash-core-viewer"; import { ClashCoreViewer } from "./mods/clash-core-viewer";
import { invoke_uwp_tool } from "@/services/cmds"; import { invoke_uwp_tool } from "@/services/cmds";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { useVerge } from "@/hooks/use-verge";
const isWIN = getSystem() === "windows"; const isWIN = getSystem() === "windows";
@@ -32,12 +34,11 @@ const SettingClash = ({ onError }: Props) => {
const { clash, version, mutateClash, patchClash } = useClash(); const { clash, version, mutateClash, patchClash } = useClash();
const { const { verge, mutateVerge, patchVerge } = useVerge();
ipv6,
"allow-lan": allowLan, const { ipv6, "allow-lan": allowLan, "log-level": logLevel } = clash ?? {};
"log-level": logLevel,
"mixed-port": mixedPort, const { enable_random_port = false, verge_mixed_port } = verge ?? {};
} = clash ?? {};
const webRef = useRef<DialogRef>(null); const webRef = useRef<DialogRef>(null);
const fieldRef = useRef<DialogRef>(null); const fieldRef = useRef<DialogRef>(null);
@@ -49,7 +50,9 @@ const SettingClash = ({ onError }: Props) => {
const onChangeData = (patch: Partial<IConfigData>) => { const onChangeData = (patch: Partial<IConfigData>) => {
mutateClash((old) => ({ ...(old! || {}), ...patch }), false); mutateClash((old) => ({ ...(old! || {}), ...patch }), false);
}; };
const onChangeVerge = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
};
return ( return (
<SettingList title={t("Clash Setting")}> <SettingList title={t("Clash Setting")}>
<WebUIViewer ref={webRef} /> <WebUIViewer ref={webRef} />
@@ -103,11 +106,32 @@ const SettingClash = ({ onError }: Props) => {
</GuardState> </GuardState>
</SettingItem> </SettingItem>
<SettingItem label={t("Mixed Port")}> <SettingItem
label={t("Mixed Port")}
extra={
<Tooltip title={t("Random Port")}>
<IconButton
color={enable_random_port ? "success" : "inherit"}
size="medium"
onClick={() => {
Notice.success(t("After restart to take effect"), 1000);
onChangeVerge({ enable_random_port: !enable_random_port });
patchVerge({ enable_random_port: !enable_random_port });
}}
>
<Shuffle
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
</Tooltip>
}
>
<TextField <TextField
disabled={enable_random_port}
autoComplete="off" autoComplete="off"
size="small" size="small"
value={mixedPort ?? 0} value={verge_mixed_port ?? 7890}
sx={{ width: 100, input: { py: "7.5px", cursor: "pointer" } }} sx={{ width: 100, input: { py: "7.5px", cursor: "pointer" } }}
onClick={(e) => { onClick={(e) => {
portRef.current?.open(); portRef.current?.open();

View File

@@ -103,6 +103,7 @@ const SettingVerge = ({ onError }: Props) => {
<MenuItem value="main_window">{t("Show Main Window")}</MenuItem> <MenuItem value="main_window">{t("Show Main Window")}</MenuItem>
<MenuItem value="system_proxy">{t("System Proxy")}</MenuItem> <MenuItem value="system_proxy">{t("System Proxy")}</MenuItem>
<MenuItem value="tun_mode">{t("Tun Mode")}</MenuItem> <MenuItem value="tun_mode">{t("Tun Mode")}</MenuItem>
<MenuItem value="disable">{t("Disable")}</MenuItem>
</Select> </Select>
</GuardState> </GuardState>
</SettingItem> </SettingItem>

View File

@@ -67,6 +67,8 @@
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "Log Level", "Log Level": "Log Level",
"Mixed Port": "Mixed Port", "Mixed Port": "Mixed Port",
"Random Port": "Random Port",
"After restart to take effect": "After restart to take effect",
"External": "External", "External": "External",
"Clash Core": "Clash Core", "Clash Core": "Clash Core",
"Grant": "Grant", "Grant": "Grant",

View File

@@ -64,6 +64,8 @@
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "Уровень логов", "Log Level": "Уровень логов",
"Mixed Port": "Смешанный порт", "Mixed Port": "Смешанный порт",
"Random Port": "Случайный порт",
"After restart to take effect": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
"Clash Core": "Ядро Clash", "Clash Core": "Ядро Clash",
"Tun Mode": "Режим туннеля", "Tun Mode": "Режим туннеля",
"Service Mode": "Режим сервиса", "Service Mode": "Режим сервиса",

View File

@@ -67,6 +67,8 @@
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "日志等级", "Log Level": "日志等级",
"Mixed Port": "端口设置", "Mixed Port": "端口设置",
"Random Port": "随机端口",
"After restart to take effect": "重启后生效",
"External": "外部控制", "External": "外部控制",
"Clash Core": "Clash 内核", "Clash Core": "Clash 内核",
"Grant": "授权", "Grant": "授权",

View File

@@ -1,3 +1,6 @@
import getSystem from "@/utils/get-system";
const OS = getSystem();
// default theme setting // default theme setting
export const defaultTheme = { export const defaultTheme = {
primary_color: "#1867c0", primary_color: "#1867c0",
@@ -8,12 +11,16 @@ export const defaultTheme = {
error_color: "#d32f2f", error_color: "#d32f2f",
warning_color: "#ed6c02", warning_color: "#ed6c02",
success_color: "#2e7d32", success_color: "#2e7d32",
font_family: `"twemoji mozilla", "Roboto", "Helvetica", "Arial", sans-serif`, background_color: "#ffffff",
font_family: `"Roboto", "Helvetica", "Arial", sans-serif, ${
OS === "windows" ? "twemoji mozilla" : ""
}`,
}; };
// dark mode // dark mode
export const defaultDarkTheme = { export const defaultDarkTheme = {
...defaultTheme, ...defaultTheme,
primary_text: "#E8E8ED", primary_text: "#E8E8ED",
background_color: "#181818",
secondary_text: "#bbbbbb", secondary_text: "#bbbbbb",
}; };

View File

@@ -3,6 +3,19 @@ import { useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useSetRecoilState } from "recoil"; import { useSetRecoilState } from "recoil";
import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material"; import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { import {
ClearRounded, ClearRounded,
@@ -19,6 +32,7 @@ import {
getRuntimeLogs, getRuntimeLogs,
deleteProfile, deleteProfile,
updateProfile, updateProfile,
reorderProfile,
} from "@/services/cmds"; } from "@/services/cmds";
import { atomLoadingCache } from "@/services/states"; import { atomLoadingCache } from "@/services/states";
import { closeAllConnections } from "@/services/api"; import { closeAllConnections } from "@/services/api";
@@ -40,7 +54,12 @@ const ProfilePage = () => {
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
const [activating, setActivating] = useState(""); const [activating, setActivating] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const { const {
profiles = {}, profiles = {},
activateSelected, activateSelected,
@@ -106,6 +125,16 @@ const ProfilePage = () => {
} }
}; };
const onDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (over) {
if (active.id !== over.id) {
await reorderProfile(active.id.toString(), over.id.toString());
mutateProfiles();
}
}
};
const onSelect = useLockFn(async (current: string, force: boolean) => { const onSelect = useLockFn(async (current: string, force: boolean) => {
if (!force && current === profiles.current) return; if (!force && current === profiles.current) return;
// 避免大多数情况下loading态闪烁 // 避免大多数情况下loading态闪烁
@@ -293,22 +322,34 @@ const ProfilePage = () => {
{t("New")} {t("New")}
</Button> </Button>
</Stack> </Stack>
<DndContext
<Box sx={{ mb: 4.5 }}> sensors={sensors}
<Grid container spacing={{ xs: 1, lg: 1 }}> collisionDetection={closestCenter}
{regularItems.map((item) => ( onDragEnd={onDragEnd}
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}> >
<ProfileItem <Box sx={{ mb: 4.5 }}>
selected={profiles.current === item.uid} <Grid container spacing={{ xs: 1, lg: 1 }}>
activating={activating === item.uid} <SortableContext
itemData={item} items={regularItems.map((x) => {
onSelect={(f) => onSelect(item.uid, f)} return x.uid;
onEdit={() => viewerRef.current?.edit(item)} })}
/> >
</Grid> {regularItems.map((item) => (
))} <Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
</Grid> <ProfileItem
</Box> id={item.uid}
selected={profiles.current === item.uid}
activating={activating === item.uid}
itemData={item}
onSelect={(f) => onSelect(item.uid, f)}
onEdit={() => viewerRef.current?.edit(item)}
/>
</Grid>
))}
</SortableContext>
</Grid>
</Box>
</DndContext>
{enhanceItems.length > 0 && ( {enhanceItems.length > 0 && (
<Grid container spacing={{ xs: 2, lg: 2 }}> <Grid container spacing={{ xs: 2, lg: 2 }}>
@@ -330,7 +371,6 @@ const ProfilePage = () => {
))} ))}
</Grid> </Grid>
)} )}
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} /> <ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
<ConfigViewer ref={configRef} /> <ConfigViewer ref={configRef} />
</BasePage> </BasePage>

View File

@@ -16,7 +16,7 @@ const SettingPage = () => {
}; };
const toGithubRepo = useLockFn(() => { const toGithubRepo = useLockFn(() => {
return openWebUrl("https://github.com/wonfen/clash-verge-rev"); return openWebUrl("https://github.com/clash-verge-rev/clash-verge-rev");
}); });
return ( return (
@@ -26,7 +26,7 @@ const SettingPage = () => {
<IconButton <IconButton
size="small" size="small"
color="inherit" color="inherit"
title="@wonfen/clash-verge-rev" title="@clash-verge-rev/clash-verge-rev"
onClick={toGithubRepo} onClick={toGithubRepo}
> >
<GitHub fontSize="inherit" /> <GitHub fontSize="inherit" />

View File

@@ -64,6 +64,13 @@ export async function importProfile(url: string) {
}); });
} }
export async function reorderProfile(activeId: string, overId: string) {
return invoke<void>("reorder_profile", {
activeId,
overId,
});
}
export async function updateProfile(index: string, option?: IProfileOption) { export async function updateProfile(index: string, option?: IProfileOption) {
return invoke<void>("update_profile", { index, option }); return invoke<void>("update_profile", { index, option });
} }

View File

@@ -166,6 +166,8 @@ interface IVergeConfig {
enable_service_mode?: boolean; enable_service_mode?: boolean;
enable_silent_start?: boolean; enable_silent_start?: boolean;
enable_system_proxy?: boolean; enable_system_proxy?: boolean;
enable_random_port?: boolean;
verge_mixed_port?: number;
enable_proxy_guard?: boolean; enable_proxy_guard?: boolean;
proxy_guard_duration?: number; proxy_guard_duration?: number;
system_proxy_bypass?: string; system_proxy_bypass?: string;

View File

@@ -47,6 +47,7 @@ export const OTHERS_FIELDS = [
"tcp-concurrent", // meta "tcp-concurrent", // meta
"enable-process", // meta "enable-process", // meta
"find-process-mode", // meta "find-process-mode", // meta
"skip-auth-prefixes", // meta
"external-controller-tls", // meta "external-controller-tls", // meta
"global-client-fingerprint", // meta "global-client-fingerprint", // meta
] as const; ] as const;