Compare commits
22 Commits
v0.8.74
...
v0.8.83-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cb5d40969 | ||
|
|
30ee6889ab | ||
|
|
fb9d0cb22c | ||
|
|
e7eb312254 | ||
|
|
c9cd80bcb3 | ||
|
|
a77b3a35e8 | ||
|
|
2d2708d7bd | ||
|
|
ef5f6dbd59 | ||
|
|
b6c7b15e3e | ||
|
|
de9c5ba9cc | ||
|
|
2aae00cf68 | ||
|
|
68be2d34a1 | ||
|
|
7895ccf720 | ||
|
|
e92900dbbd | ||
|
|
eada271c49 | ||
|
|
5dda2854be | ||
|
|
5184ed6fc7 | ||
|
|
4e679f776e | ||
|
|
96328f66e9 | ||
|
|
3eb14ab8a1 | ||
|
|
c6266b7917 | ||
|
|
6c27f2e2f1 |
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: 问题反馈 / Bug report
|
||||
title: "[BUG] "
|
||||
description: 反馈你遇到的问题 / Report the issue you are experiencing
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前,请确认以下事项:
|
||||
1. 请务必给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请务必按照模板规范详细描述问题,否则issue将会被直接关闭
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述 / Describe the bug
|
||||
description: 详细清晰地描述你遇到的问题,并配合截图 / Describe the problem you encountered in detail and clearly, and provide screenshots
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 软件版本 / Version
|
||||
description: 请提供FlClash的具体版本 / Please provide the specific version of FlClash.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 复现步骤 / To Reproduce
|
||||
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 操作系统 / OS
|
||||
options:
|
||||
- Android
|
||||
- Windows
|
||||
- MacOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: 操作系统版本 / OS Version
|
||||
description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
||||
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug / Please provide a complete or relevant Debug log (please adjust it to debug in the left menu of software-> Settings-> Log Level)
|
||||
validations:
|
||||
required: true
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: 讨论交流 / Communication
|
||||
url: https://t.me/+G-veVtwBOl4wODc1
|
||||
about: 在 Telegram 群组中与其他用户讨论交流 / Communicate with other users in the Telegram group
|
||||
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: 功能请求 / Feature request
|
||||
title: "[Feature] "
|
||||
description: 提出你的功能请求 / Propose your feature request
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前,请确认以下事项:
|
||||
1. 请务必给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请务必按照模板规范详细描述问题,否则issue将会被直接关闭
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 功能描述 / Feature description
|
||||
description: 详细清晰地描述你的功能请求 / A clear and concise description of what the feature is
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 使用场景 / Use case
|
||||
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: os-labels
|
||||
attributes:
|
||||
label: 适用系统 / Target OS
|
||||
description: 请选择该功能适用的操作系统(至少选择一个) / Please select the operating system(s) for this feature request (select at least one)
|
||||
options:
|
||||
- label: Android
|
||||
- label: Windows
|
||||
- label: MacOS
|
||||
- label: Linux
|
||||
validations:
|
||||
required: true
|
||||
66
.github/workflows/build.yaml
vendored
66
.github/workflows/build.yaml
vendored
@@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
env:
|
||||
IS_STABLE: ${{ !contains(github.ref, '-') }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -25,29 +27,27 @@ jobs:
|
||||
- platform: macos
|
||||
os: macos-latest
|
||||
arch: arm64
|
||||
- platform: windows
|
||||
os: windows-11-arm
|
||||
arch: arm64
|
||||
- platform: linux
|
||||
os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Setup rust
|
||||
if: startsWith(matrix.os, 'windows-11-arm')
|
||||
run: |
|
||||
Invoke-WebRequest -Uri "https://win.rustup.rs/aarch64" -OutFile rustup-init.exe
|
||||
.\rustup-init.exe -y --default-toolchain stable
|
||||
$cargoPath = "$env:USERPROFILE\.cargo\bin"
|
||||
Add-Content $env:GITHUB_PATH $cargoPath
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup JAVA
|
||||
if: startsWith(matrix.platform,'android')
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Setup NDK
|
||||
if: startsWith(matrix.platform,'android')
|
||||
uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r26b
|
||||
add-to-path: true
|
||||
link-to-sdk: true
|
||||
|
||||
- name: Setup Android Signing
|
||||
if: startsWith(matrix.platform,'android')
|
||||
run: |
|
||||
@@ -56,26 +56,24 @@ jobs:
|
||||
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
|
||||
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
|
||||
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version: '1.24.0'
|
||||
cache-dependency-path: |
|
||||
core/go.sum
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: 3.24.5
|
||||
channel: stable
|
||||
channel: ${{ (startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) && 'master' || 'stable' }}
|
||||
cache: true
|
||||
|
||||
- name: Get Flutter Dependency
|
||||
run: flutter pub get
|
||||
|
||||
- name: Setup
|
||||
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }}
|
||||
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }}
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -89,14 +87,13 @@ jobs:
|
||||
needs: [ build ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
uses: actions/checkout@v4
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: refs/heads/main
|
||||
|
||||
- name: Generate
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
|
||||
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
|
||||
@@ -128,7 +125,7 @@ jobs:
|
||||
cat NEW_CHANGELOG.md > CHANGELOG.md
|
||||
|
||||
- name: Commit
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
git add CHANGELOG.md
|
||||
if ! git diff --cached --quiet; then
|
||||
@@ -204,32 +201,41 @@ jobs:
|
||||
env:
|
||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
python release.py
|
||||
python release_telegram.py
|
||||
|
||||
- name: Patch release.md
|
||||
run: |
|
||||
version=$(echo "${{ github.ref_name }}" | sed 's/^v//')
|
||||
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
|
||||
|
||||
- name: Generate sha256
|
||||
if: env.IS_STABLE == 'true'
|
||||
run: |
|
||||
cd ./dist
|
||||
for file in $(find . -type f -not -name "*.sha256"); do
|
||||
sha256sum "$file" > "${file}.sha256"
|
||||
done
|
||||
|
||||
- name: Release
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./dist/*
|
||||
body_path: './release.md'
|
||||
|
||||
- name: Create Fdroid Source Dir
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
mkdir -p ./tmp
|
||||
cp ./dist/*android-arm64-v8a* ./tmp/ || true
|
||||
echo "Files copied successfully"
|
||||
|
||||
- name: Push to fdroid repo
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
uses: cpina/github-action-push-to-another-repository@v1.7.2
|
||||
env:
|
||||
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||
@@ -239,7 +245,7 @@ jobs:
|
||||
destination-repository-name: FlClash-fdroid-repo
|
||||
user-name: 'github-actions[bot]'
|
||||
user-email: 'github-actions[bot]@users.noreply.github.com'
|
||||
target-branch: action-pr
|
||||
target-branch: main
|
||||
commit-message: Update from ${{ github.ref_name }}
|
||||
target-directory: /tmp/
|
||||
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,8 +1,10 @@
|
||||
[submodule "core/Clash.Meta"]
|
||||
path = core/Clash.Meta
|
||||
url = git@github.com:chen08209/Clash.Meta.git
|
||||
branch = FlClash-Alpha
|
||||
branch = FlClash
|
||||
[submodule "plugins/flutter_distributor"]
|
||||
path = plugins/flutter_distributor
|
||||
url = git@github.com:chen08209/flutter_distributor.git
|
||||
branch = FlClash
|
||||
|
||||
|
||||
|
||||
94
CHANGELOG.md
94
CHANGELOG.md
@@ -1,3 +1,97 @@
|
||||
## v0.8.82
|
||||
|
||||
- Optimize android vpn performance
|
||||
|
||||
- Add custom primary color and color scheme
|
||||
|
||||
- Add linux nad windows arm release
|
||||
|
||||
- Optimize requests and logs page
|
||||
|
||||
- Fix map input page delete issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.81
|
||||
|
||||
- Add rule override
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.80
|
||||
|
||||
- Optimize dashboard performance
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Fix unselected proxy group delay issues
|
||||
|
||||
- Fix asn url issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.79
|
||||
|
||||
- Fix tab delay view issues
|
||||
|
||||
- Fix tray action issues
|
||||
|
||||
- Fix get profile redirect client ua issues
|
||||
|
||||
- Fix proxy card delay view issues
|
||||
|
||||
- Add Russian, Japanese adaptation
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.78
|
||||
|
||||
- Fix list form input view issues
|
||||
|
||||
- Fix traffic view issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.77
|
||||
|
||||
- Optimize performance
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize core stability
|
||||
|
||||
- Fix linux tun authority check error
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Fix scroll physics error
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.75
|
||||
|
||||
- Add windows storage corruption detection
|
||||
|
||||
- Fix core crash caused by windows resource manager restart
|
||||
|
||||
- Optimize logs, requests, access to pages
|
||||
|
||||
- Fix macos bypass domain issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.74
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.73
|
||||
|
||||
- Update popup menu
|
||||
|
||||
10
Makefile
Normal file
10
Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
android_arm64:
|
||||
dart ./setup.dart android --arch arm64
|
||||
macos_arm64:
|
||||
dart ./setup.dart macos --arch arm64
|
||||
android_app:
|
||||
dart ./setup.dart android
|
||||
android_arm64_core:
|
||||
dart ./setup.dart android --arch arm64 --out core
|
||||
macos_arm64_core:
|
||||
dart ./setup.dart macos --arch arm64 --out core
|
||||
@@ -1,29 +1 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import com.android.build.gradle.tasks.MergeSourceSetFolders
|
||||
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
@@ -33,8 +31,8 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
|
||||
|
||||
android {
|
||||
namespace "com.follow.clash"
|
||||
compileSdkVersion 34
|
||||
ndkVersion "27.1.12297006"
|
||||
compileSdk 35
|
||||
ndkVersion = "28.0.13004108"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -48,6 +46,7 @@ android {
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (isRelease) {
|
||||
release {
|
||||
@@ -63,7 +62,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "com.follow.clash"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@@ -84,31 +83,15 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('copyNativeLibs', Copy) {
|
||||
delete('src/main/jniLibs')
|
||||
from('../../libclash/android')
|
||||
into('src/main/jniLibs')
|
||||
}
|
||||
|
||||
tasks.withType(MergeSourceSetFolders).configureEach {
|
||||
dependsOn copyNativeLibs
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":core")
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") {
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
|
||||
exclude group: "com.google.guava", module: "guava"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
afterEvaluate {
|
||||
assembleDebug.dependsOn copyNativeLibs
|
||||
|
||||
assembleRelease.dependsOn copyNativeLibs
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<application android:label="FlClash Debug" tools:replace="android:label">
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label">
|
||||
<service
|
||||
android:name=".services.FlClashTileService"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label">
|
||||
</service>
|
||||
android:name=".services.FlClashTileService"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label"
|
||||
tools:targetApi="24" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -10,14 +10,12 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
tools:ignore="SystemPermissionTypo" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
@@ -46,6 +44,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
@@ -64,7 +63,9 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="false" />
|
||||
|
||||
<activity
|
||||
android:name=".TempActivity"
|
||||
@@ -87,7 +88,6 @@
|
||||
<service
|
||||
android:name=".services.FlClashTileService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:label="FlClash"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
@@ -125,7 +125,7 @@
|
||||
<service
|
||||
android:name=".services.FlClashVpnService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
@@ -138,7 +138,7 @@
|
||||
<service
|
||||
android:name=".services.FlClashService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
android:foregroundServiceType="dataSync">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="service" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.follow.clash;
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context;
|
||||
import android.content.Context
|
||||
|
||||
class FlClashApplication : Application() {
|
||||
companion object {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
import io.flutter.FlutterInjector
|
||||
@@ -22,6 +20,10 @@ enum class RunState {
|
||||
object GlobalState {
|
||||
val runLock = ReentrantLock()
|
||||
|
||||
const val NOTIFICATION_CHANNEL = "FlClash"
|
||||
|
||||
const val NOTIFICATION_ID = 1
|
||||
|
||||
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
|
||||
var flutterEngine: FlutterEngine? = null
|
||||
private var serviceEngine: FlutterEngine? = null
|
||||
|
||||
@@ -27,7 +27,6 @@ import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
suspend fun Drawable.getBase64(): String {
|
||||
val drawable = this
|
||||
return withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -4,5 +4,5 @@ data class Package(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
val isSystem: Boolean,
|
||||
val firstInstallTime: Long,
|
||||
val lastUpdateTime: Long,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ enum class AccessControlMode {
|
||||
}
|
||||
|
||||
data class AccessControl(
|
||||
val enable: Boolean,
|
||||
val mode: AccessControlMode,
|
||||
val acceptList: List<String>,
|
||||
val rejectList: List<String>,
|
||||
@@ -17,7 +18,7 @@ data class CIDR(val address: InetAddress, val prefixLength: Int)
|
||||
data class VpnOptions(
|
||||
val enable: Boolean,
|
||||
val port: Int,
|
||||
val accessControl: AccessControl?,
|
||||
val accessControl: AccessControl,
|
||||
val allowBypass: Boolean,
|
||||
val systemProxy: Boolean,
|
||||
val bypassDomain: List<String>,
|
||||
|
||||
@@ -37,7 +37,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
@@ -292,17 +291,19 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
private fun getPackages(): List<Package> {
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager
|
||||
if (packages.isNotEmpty()) return packages
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
||||
it.packageName != FlClashApplication.getAppContext().packageName
|
||||
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
|
||||
?.filter {
|
||||
it.packageName != FlClashApplication.getAppContext().packageName && (
|
||||
it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
)
|
||||
|
||||
}?.map {
|
||||
}?.map {
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
|
||||
firstInstallTime = it.firstInstallTime
|
||||
label = it.applicationInfo?.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
|
||||
lastUpdateTime = it.lastUpdateTime
|
||||
)
|
||||
}?.let { packages.addAll(it) }
|
||||
return packages
|
||||
@@ -354,7 +355,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
suspend fun getText(text: String): String? {
|
||||
return withContext(Dispatchers.Default){
|
||||
return withContext(Dispatchers.Default) {
|
||||
channel.awaitResult<String>("getText", text)
|
||||
}
|
||||
}
|
||||
@@ -392,31 +393,33 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}.forEach {
|
||||
if (it.name.matches(chinaAppRegex)) return true
|
||||
}
|
||||
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
|
||||
for (packageEntry in it.entries()) {
|
||||
if (packageEntry.name.startsWith("firebase-")) return false
|
||||
}
|
||||
for (packageEntry in it.entries()) {
|
||||
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
|
||||
".dex"
|
||||
))
|
||||
) {
|
||||
continue
|
||||
packageInfo.applicationInfo?.publicSourceDir?.let {
|
||||
ZipFile(File(it)).use {
|
||||
for (packageEntry in it.entries()) {
|
||||
if (packageEntry.name.startsWith("firebase-")) return false
|
||||
}
|
||||
if (packageEntry.size > 15000000) {
|
||||
return true
|
||||
}
|
||||
val input = it.getInputStream(packageEntry).buffered()
|
||||
val dexFile = try {
|
||||
DexBackedDexFile.fromInputStream(null, input)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
for (clazz in dexFile.classes) {
|
||||
val clazzName =
|
||||
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
|
||||
.replace("$", ".")
|
||||
if (clazzName.matches(chinaAppRegex)) return true
|
||||
for (packageEntry in it.entries()) {
|
||||
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
|
||||
".dex"
|
||||
))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (packageEntry.size > 15000000) {
|
||||
return true
|
||||
}
|
||||
val input = it.getInputStream(packageEntry).buffered()
|
||||
val dexFile = try {
|
||||
DexBackedDexFile.fromInputStream(null, input)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
for (clazz in dexFile.classes) {
|
||||
val clazzName =
|
||||
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
|
||||
.replace("$", ".")
|
||||
if (clazzName.matches(chinaAppRegex)) return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.google.gson.Gson
|
||||
@@ -53,7 +52,6 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun handleDestroy() {
|
||||
GlobalState.getCurrentVPNPlugin()?.handleStop()
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,9 @@ import androidx.core.content.getSystemService
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.extensions.awaitResult
|
||||
import com.follow.clash.extensions.getProtocol
|
||||
import com.follow.clash.extensions.resolveDns
|
||||
import com.follow.clash.models.Process
|
||||
import com.follow.clash.models.StartForegroundParams
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.follow.clash.services.BaseServiceInterface
|
||||
@@ -40,10 +39,12 @@ import kotlin.concurrent.withLock
|
||||
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
private var flClashService: BaseServiceInterface? = null
|
||||
private lateinit var options: VpnOptions
|
||||
private var options: VpnOptions? = null
|
||||
private var isBind: Boolean = false
|
||||
private lateinit var scope: CoroutineScope
|
||||
private var lastStartForegroundParams: StartForegroundParams? = null
|
||||
private var timerJob: Job? = null
|
||||
private val uidPageNameMap = mutableMapOf<Int, String>()
|
||||
|
||||
private val connectivity by lazy {
|
||||
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
|
||||
@@ -51,6 +52,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
isBind = true
|
||||
flClashService = when (service) {
|
||||
is FlClashVpnService.LocalBinder -> service.getService()
|
||||
is FlClashService.LocalBinder -> service.getService()
|
||||
@@ -60,6 +62,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
isBind = false
|
||||
flClashService = null
|
||||
}
|
||||
}
|
||||
@@ -90,60 +93,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"setProtect" -> {
|
||||
val fd = call.argument<Int>("fd")
|
||||
if (fd != null) {
|
||||
if (flClashService is FlClashVpnService) {
|
||||
(flClashService as FlClashVpnService).protect(fd)
|
||||
}
|
||||
result.success(true)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
"resolverProcess" -> {
|
||||
val data = call.argument<String>("data")
|
||||
val process = if (data != null) Gson().fromJson(
|
||||
data, Process::class.java
|
||||
) else null
|
||||
val metadata = process?.metadata
|
||||
if (metadata == null) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
val protocol = metadata.getProtocol()
|
||||
if (protocol == null) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
|
||||
val dst = InetSocketAddress(
|
||||
metadata.destinationIP.ifEmpty { metadata.host },
|
||||
metadata.destinationPort
|
||||
)
|
||||
val uid = try {
|
||||
connectivity?.getConnectionOwnerUid(protocol, src, dst)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (uid == null || uid == -1) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
val packages =
|
||||
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
|
||||
result.success(packages?.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
@@ -151,6 +100,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
fun handleStart(options: VpnOptions): Boolean {
|
||||
if (options.enable != this.options?.enable) {
|
||||
this.flClashService = null
|
||||
}
|
||||
this.options = options
|
||||
when (options.enable) {
|
||||
true -> handleStartVpn()
|
||||
@@ -160,10 +112,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()
|
||||
?.requestVpnPermission {
|
||||
handleStartService()
|
||||
}
|
||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
|
||||
handleStartService()
|
||||
}
|
||||
}
|
||||
|
||||
fun requestGc() {
|
||||
@@ -217,8 +168,10 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
try {
|
||||
if (GlobalState.runState.value != RunState.START) return
|
||||
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
|
||||
val startForegroundParams = Gson().fromJson(
|
||||
val startForegroundParams = if (data != null) Gson().fromJson(
|
||||
data, StartForegroundParams::class.java
|
||||
) else StartForegroundParams(
|
||||
title = "", content = ""
|
||||
)
|
||||
if (lastStartForegroundParams != startForegroundParams) {
|
||||
lastStartForegroundParams = startForegroundParams
|
||||
@@ -233,6 +186,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun startForegroundJob() {
|
||||
stopForegroundJob()
|
||||
timerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
while (isActive) {
|
||||
startForeground()
|
||||
@@ -254,26 +208,58 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.START) return
|
||||
GlobalState.runState.value = RunState.START
|
||||
val fd = flClashService?.start(options)
|
||||
flutterMethodChannel.invokeMethod(
|
||||
"started", fd
|
||||
val fd = flClashService?.start(options!!)
|
||||
Core.startTun(
|
||||
fd = fd ?: 0,
|
||||
protect = this::protect,
|
||||
resolverProcess = this::resolverProcess,
|
||||
)
|
||||
startForegroundJob();
|
||||
startForegroundJob()
|
||||
}
|
||||
}
|
||||
|
||||
private fun protect(fd: Int): Boolean {
|
||||
return (flClashService as? FlClashVpnService)?.protect(fd) == true
|
||||
}
|
||||
|
||||
private fun resolverProcess(
|
||||
protocol: Int,
|
||||
source: InetSocketAddress,
|
||||
target: InetSocketAddress,
|
||||
uid: Int,
|
||||
): String {
|
||||
val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1
|
||||
} else {
|
||||
uid
|
||||
}
|
||||
if (nextUid == -1) {
|
||||
return ""
|
||||
}
|
||||
if (!uidPageNameMap.containsKey(nextUid)) {
|
||||
uidPageNameMap[nextUid] =
|
||||
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(nextUid)
|
||||
?.first() ?: ""
|
||||
}
|
||||
return uidPageNameMap[nextUid] ?: ""
|
||||
}
|
||||
|
||||
fun handleStop() {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
stopForegroundJob()
|
||||
Core.stopTun()
|
||||
flClashService?.stop()
|
||||
GlobalState.handleTryDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
val intent = when (options.enable) {
|
||||
if (isBind) {
|
||||
FlClashApplication.getAppContext().unbindService(connection)
|
||||
}
|
||||
val intent = when (options?.enable == true) {
|
||||
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
|
||||
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.extensions.getActionPendingIntent
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import io.flutter.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
interface BaseServiceInterface {
|
||||
|
||||
@@ -9,4 +29,71 @@ interface BaseServiceInterface {
|
||||
fun stop()
|
||||
|
||||
suspend fun startForeground(title: String, content: String)
|
||||
}
|
||||
|
||||
fun Service.createFlClashNotificationBuilder(): Deferred<NotificationCompat.Builder> =
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
val intent = Intent(this@createFlClashNotificationBuilder, MainActivity::class.java)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@createFlClashNotificationBuilder,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@createFlClashNotificationBuilder, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(
|
||||
NotificationCompat.Builder(
|
||||
this@createFlClashNotificationBuilder, GlobalState.NOTIFICATION_CHANNEL
|
||||
)
|
||||
) {
|
||||
setSmallIcon(R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
addAction(
|
||||
0, stopText, getActionPendingIntent("STOP")
|
||||
)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
fun Service.startForeground(notification: Notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL)
|
||||
if (channel == null) {
|
||||
Log.d("[FlClash]","createNotificationChannel===>")
|
||||
channel = NotificationChannel(
|
||||
GlobalState.NOTIFICATION_CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("[FlClash]","startForeground===>")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
try {
|
||||
startForeground(
|
||||
GlobalState.NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
startForeground(GlobalState.NOTIFICATION_ID, notification)
|
||||
}
|
||||
} else {
|
||||
startForeground(GlobalState.NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,51 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.extensions.getActionPendingIntent
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
|
||||
class FlClashService : Service(), BaseServiceInterface {
|
||||
|
||||
override fun start(options: VpnOptions) = 0
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
private var cachedBuilder: NotificationCompat.Builder? = null
|
||||
|
||||
private suspend fun notificationBuilder(): NotificationCompat.Builder {
|
||||
if (cachedBuilder == null) {
|
||||
cachedBuilder = createFlClashNotificationBuilder().await()
|
||||
}
|
||||
return cachedBuilder!!
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
startForeground(
|
||||
notificationBuilder()
|
||||
.setContentTitle(title)
|
||||
.setContentText(content).build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
GlobalState.getCurrentVPNPlugin()?.requestGc()
|
||||
}
|
||||
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
@@ -38,87 +60,8 @@ class FlClashService : Service(), BaseServiceInterface {
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
|
||||
val intent = Intent(
|
||||
this@FlClashService, MainActivity::class.java
|
||||
)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this@FlClashService, CHANNEL)) {
|
||||
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
addAction(
|
||||
0,
|
||||
stopText, // 使用 suspend 函数获取的文本
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
|
||||
override fun start(options: VpnOptions) = 0
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
getNotificationBuilder()
|
||||
.setContentTitle(title)
|
||||
.setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
override fun onDestroy() {
|
||||
stop()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Binder
|
||||
@@ -17,18 +13,13 @@ import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.extensions.getActionPendingIntent
|
||||
import com.follow.clash.extensions.getIpv4RouteAddress
|
||||
import com.follow.clash.extensions.getIpv6RouteAddress
|
||||
import com.follow.clash.extensions.toCIDR
|
||||
import com.follow.clash.models.AccessControlMode
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@@ -45,9 +36,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
val routeAddress = options.getIpv4RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}")
|
||||
addRoute(i.address, i.prefixLength)
|
||||
try {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d(
|
||||
"addRoute4",
|
||||
"address: ${i.address} prefixLength:${i.prefixLength}"
|
||||
)
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
addRoute("0.0.0.0", 0)
|
||||
}
|
||||
} else {
|
||||
addRoute("0.0.0.0", 0)
|
||||
@@ -58,9 +56,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
val routeAddress = options.getIpv6RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}")
|
||||
addRoute(i.address, i.prefixLength)
|
||||
try {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d(
|
||||
"addRoute6",
|
||||
"address: ${i.address} prefixLength:${i.prefixLength}"
|
||||
)
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
addRoute("::", 0)
|
||||
}
|
||||
} else {
|
||||
addRoute("::", 0)
|
||||
@@ -68,17 +73,19 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
}
|
||||
addDnsServer(options.dnsServerAddress)
|
||||
setMtu(9000)
|
||||
options.accessControl?.let { accessControl ->
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
addAllowedApplication(it)
|
||||
options.accessControl.let { accessControl ->
|
||||
if (accessControl.enable) {
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
addAllowedApplication(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccessControlMode.rejectSelected -> {
|
||||
(accessControl.rejectList - packageName).forEach {
|
||||
addDisallowedApplication(it)
|
||||
AccessControlMode.rejectSelected -> {
|
||||
(accessControl.rejectList - packageName).forEach {
|
||||
addDisallowedApplication(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,78 +119,22 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
private var cachedBuilder: NotificationCompat.Builder? = null
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
val intent = Intent(this@FlClashVpnService, MainActivity::class.java)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashVpnService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashVpnService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) {
|
||||
setSmallIcon(R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
addAction(
|
||||
0,
|
||||
stopText,
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
private suspend fun notificationBuilder(): NotificationCompat.Builder {
|
||||
if (cachedBuilder == null) {
|
||||
cachedBuilder = createFlClashNotificationBuilder().await()
|
||||
}
|
||||
return cachedBuilder!!
|
||||
}
|
||||
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
getNotificationBuilder()
|
||||
startForeground(
|
||||
notificationBuilder()
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
.setContentText(content).build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
|
||||
@@ -24,6 +24,7 @@ subprojects {
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
project.evaluationDependsOn(':core')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
|
||||
1
android/core/.gitignore
vendored
Normal file
1
android/core/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
65
android/core/build.gradle.kts
Normal file
65
android/core/build.gradle.kts
Normal file
@@ -0,0 +1,65 @@
|
||||
import com.android.build.gradle.tasks.MergeSourceSetFolders
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.follow.clash.core"
|
||||
compileSdk = 35
|
||||
ndkVersion = "28.0.13004108"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
jniLibs.srcDirs("src/main/jniLibs")
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
val copyNativeLibs by tasks.register<Copy>("copyNativeLibs") {
|
||||
doFirst {
|
||||
delete("src/main/jniLibs")
|
||||
}
|
||||
from("../../libclash/android")
|
||||
into("src/main/jniLibs")
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(copyNativeLibs)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.16.0")
|
||||
}
|
||||
0
android/core/consumer-rules.pro
Normal file
0
android/core/consumer-rules.pro
Normal file
21
android/core/proguard-rules.pro
vendored
Normal file
21
android/core/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
4
android/core/src/main/AndroidManifest.xml
Normal file
4
android/core/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
45
android/core/src/main/cpp/CMakeLists.txt
Normal file
45
android/core/src/main/cpp/CMakeLists.txt
Normal file
@@ -0,0 +1,45 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
|
||||
project("core")
|
||||
|
||||
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
|
||||
|
||||
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
add_compile_options(-O3)
|
||||
|
||||
add_compile_options(-flto)
|
||||
|
||||
add_compile_options(-g0)
|
||||
|
||||
add_compile_options(-ffunction-sections -fdata-sections)
|
||||
|
||||
add_compile_options(-fno-exceptions -fno-rtti)
|
||||
|
||||
add_link_options(
|
||||
-flto
|
||||
-Wl,--gc-sections
|
||||
-Wl,--strip-all
|
||||
-Wl,--exclude-libs=ALL
|
||||
)
|
||||
endif ()
|
||||
|
||||
set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so")
|
||||
|
||||
message("LIB_CLASH_PATH ${LIB_CLASH_PATH}")
|
||||
if (EXISTS ${LIB_CLASH_PATH})
|
||||
message("Found libclash.so for ABI ${ANDROID_ABI}")
|
||||
add_compile_definitions(LIBCLASH)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
|
||||
link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
jni_helper.cpp
|
||||
core.cpp)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
clash)
|
||||
else ()
|
||||
message("Not found libclash.so for ABI ${ANDROID_ABI}")
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
jni_helper.cpp
|
||||
core.cpp)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME})
|
||||
endif ()
|
||||
75
android/core/src/main/cpp/core.cpp
Normal file
75
android/core/src/main/cpp/core.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include <jni.h>
|
||||
|
||||
#ifdef LIBCLASH
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
#include "jni_helper.h"
|
||||
#include "libclash.h"
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) {
|
||||
auto interface = new_global(cb);
|
||||
startTUN(fd, interface);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
|
||||
stopTun();
|
||||
}
|
||||
|
||||
|
||||
static jmethodID m_tun_interface_protect;
|
||||
static jmethodID m_tun_interface_resolve_process;
|
||||
|
||||
|
||||
static void release_jni_object_impl(void *obj) {
|
||||
ATTACH_JNI();
|
||||
del_global((jobject) obj);
|
||||
}
|
||||
|
||||
static void call_tun_interface_protect_impl(void *tun_interface, int fd) {
|
||||
ATTACH_JNI();
|
||||
env->CallVoidMethod((jobject) tun_interface,
|
||||
(jmethodID) m_tun_interface_protect,
|
||||
(jint) fd);
|
||||
}
|
||||
|
||||
static const char*
|
||||
call_tun_interface_resolve_process_impl(void *tun_interface, int protocol,
|
||||
const char *source,
|
||||
const char *target,
|
||||
int uid) {
|
||||
ATTACH_JNI();
|
||||
jstring packageName = (jstring)env->CallObjectMethod((jobject) tun_interface,
|
||||
(jmethodID) m_tun_interface_resolve_process,
|
||||
(jint) protocol,
|
||||
(jstring) new_string(source),
|
||||
(jstring) new_string(target),
|
||||
(jint) uid);
|
||||
return get_string(packageName);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||
JNIEnv *env = nullptr;
|
||||
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
|
||||
return JNI_ERR;
|
||||
}
|
||||
|
||||
initialize_jni(vm, env);
|
||||
|
||||
jclass c_tun_interface = find_class("com/follow/clash/core/TunInterface");
|
||||
|
||||
m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)V");
|
||||
m_tun_interface_resolve_process = find_method(c_tun_interface, "resolverProcess",
|
||||
"(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
|
||||
|
||||
registerCallbacks(&call_tun_interface_protect_impl,
|
||||
&call_tun_interface_resolve_process_impl,
|
||||
&release_jni_object_impl);
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
#endif
|
||||
70
android/core/src/main/cpp/jni_helper.cpp
Normal file
70
android/core/src/main/cpp/jni_helper.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include "jni_helper.h"
|
||||
|
||||
#include <malloc.h>
|
||||
#include <cstring>
|
||||
|
||||
static JavaVM *global_vm;
|
||||
|
||||
static jclass c_string;
|
||||
static jmethodID m_new_string;
|
||||
static jmethodID m_get_bytes;
|
||||
|
||||
void initialize_jni(JavaVM *vm, JNIEnv *env) {
|
||||
global_vm = vm;
|
||||
|
||||
c_string = (jclass) new_global(find_class("java/lang/String"));
|
||||
m_new_string = find_method(c_string, "<init>", "([B)V");
|
||||
m_get_bytes = find_method(c_string, "getBytes", "()[B");
|
||||
}
|
||||
|
||||
JavaVM *global_java_vm() {
|
||||
return global_vm;
|
||||
}
|
||||
|
||||
char *jni_get_string(JNIEnv *env, jstring str) {
|
||||
auto array = (jbyteArray) env->CallObjectMethod(str, m_get_bytes);
|
||||
int length = env->GetArrayLength(array);
|
||||
char *content = (char *) malloc(length + 1);
|
||||
env->GetByteArrayRegion(array, 0, length, (jbyte *) content);
|
||||
content[length] = 0;
|
||||
return content;
|
||||
}
|
||||
|
||||
jstring jni_new_string(JNIEnv *env, const char *str) {
|
||||
auto length = (int) strlen(str);
|
||||
jbyteArray array = env->NewByteArray(length);
|
||||
env->SetByteArrayRegion(array, 0, length, (const jbyte *) str);
|
||||
return (jstring) env->NewObject(c_string, m_new_string, array);
|
||||
}
|
||||
|
||||
int jni_catch_exception(JNIEnv *env) {
|
||||
int result = env->ExceptionCheck();
|
||||
if (result) {
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void jni_attach_thread(struct scoped_jni *jni) {
|
||||
JavaVM *vm = global_java_vm();
|
||||
if (vm->GetEnv((void **) &jni->env, JNI_VERSION_1_6) == JNI_OK) {
|
||||
jni->require_release = 0;
|
||||
return;
|
||||
}
|
||||
if (vm->AttachCurrentThread(&jni->env, nullptr) != JNI_OK) {
|
||||
abort();
|
||||
}
|
||||
jni->require_release = 1;
|
||||
}
|
||||
|
||||
void jni_detach_thread(struct scoped_jni *jni) {
|
||||
JavaVM *vm = global_java_vm();
|
||||
if (jni->require_release) {
|
||||
vm->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
|
||||
void release_string(char **str) {
|
||||
free(*str);
|
||||
}
|
||||
39
android/core/src/main/cpp/jni_helper.h
Normal file
39
android/core/src/main/cpp/jni_helper.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <malloc.h>
|
||||
|
||||
struct scoped_jni {
|
||||
JNIEnv *env;
|
||||
int require_release;
|
||||
};
|
||||
|
||||
extern void initialize_jni(JavaVM *vm, JNIEnv *env);
|
||||
|
||||
extern jstring jni_new_string(JNIEnv *env, const char *str);
|
||||
|
||||
extern char *jni_get_string(JNIEnv *env, jstring str);
|
||||
|
||||
extern int jni_catch_exception(JNIEnv *env);
|
||||
|
||||
extern void jni_attach_thread(struct scoped_jni *jni);
|
||||
|
||||
extern void jni_detach_thread(struct scoped_jni *env);
|
||||
|
||||
extern void release_string(char **str);
|
||||
|
||||
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
|
||||
struct scoped_jni _jni; \
|
||||
jni_attach_thread(&_jni); \
|
||||
JNIEnv *env = _jni.env
|
||||
|
||||
#define scoped_string __attribute__((cleanup(release_string))) char*
|
||||
|
||||
#define find_class(name) env->FindClass(name)
|
||||
#define find_method(cls, name, signature) env->GetMethodID(cls, name, signature)
|
||||
#define new_global(obj) env->NewGlobalRef(obj)
|
||||
#define del_global(obj) env->DeleteGlobalRef(obj)
|
||||
#define get_string(jstr) jni_get_string(env, jstr)
|
||||
#define new_string(cstr) jni_new_string(env, cstr)
|
||||
50
android/core/src/main/java/com/follow/clash/core/Core.kt
Normal file
50
android/core/src/main/java/com/follow/clash/core/Core.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.follow.clash.core
|
||||
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URL
|
||||
|
||||
data object Core {
|
||||
private external fun startTun(
|
||||
fd: Int,
|
||||
cb: TunInterface
|
||||
)
|
||||
|
||||
private fun parseInetSocketAddress(address: String): InetSocketAddress {
|
||||
val url = URL("https://$address")
|
||||
|
||||
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
|
||||
}
|
||||
|
||||
fun startTun(
|
||||
fd: Int,
|
||||
protect: (Int) -> Boolean,
|
||||
resolverProcess: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress, uid: Int) -> String
|
||||
) {
|
||||
startTun(fd, object : TunInterface {
|
||||
override fun protect(fd: Int) {
|
||||
protect(fd)
|
||||
}
|
||||
|
||||
override fun resolverProcess(
|
||||
protocol: Int,
|
||||
source: String,
|
||||
target: String,
|
||||
uid: Int
|
||||
): String {
|
||||
return resolverProcess(
|
||||
protocol,
|
||||
parseInetSocketAddress(source),
|
||||
parseInetSocketAddress(target),
|
||||
uid,
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
external fun stopTun()
|
||||
|
||||
init {
|
||||
System.loadLibrary("core")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.follow.clash.core
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
interface TunInterface {
|
||||
fun protect(fd: Int)
|
||||
fun resolverProcess(protocol: Int, source: String, target: String, uid: Int): String
|
||||
}
|
||||
@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
kotlin_version=1.9.22
|
||||
agp_version=8.2.1
|
||||
agp_version=8.9.1
|
||||
|
||||
@@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
|
||||
|
||||
@@ -24,3 +24,4 @@ plugins {
|
||||
}
|
||||
|
||||
include ":app"
|
||||
include ':core'
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
"other": "Other",
|
||||
"about": "About",
|
||||
"en": "English",
|
||||
"ja": "Japanese",
|
||||
"ru": "Russian",
|
||||
"zh_CN": "Simplified Chinese",
|
||||
"theme": "Theme",
|
||||
"themeDesc": "Set dark mode,adjust the color",
|
||||
@@ -121,7 +123,6 @@
|
||||
"project": "Project",
|
||||
"core": "Core",
|
||||
"tabAnimation": "Tab animation",
|
||||
"tabAnimationDesc": "When enabled, the home tab will add a toggle animation",
|
||||
"desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.",
|
||||
"startVpn": "Starting VPN...",
|
||||
"stopVpn": "Stopping VPN...",
|
||||
@@ -179,7 +180,6 @@
|
||||
"requests": "Requests",
|
||||
"requestsDesc": "View recently request records",
|
||||
"findProcessMode": "Find process",
|
||||
"findProcessModeDesc": "There is a risk of flashback after opening",
|
||||
"init": "Init",
|
||||
"infiniteTime": "Long term effective",
|
||||
"expirationTime": "Expiration time",
|
||||
@@ -215,12 +215,12 @@
|
||||
"go": "Go",
|
||||
"externalLink": "External link",
|
||||
"otherContributors": "Other contributors",
|
||||
"autoCloseConnections": "Auto lose connections",
|
||||
"autoCloseConnections": "Auto close connections",
|
||||
"autoCloseConnectionsDesc": "Auto close connections after change node",
|
||||
"onlyStatisticsProxy": "Only statistics proxy",
|
||||
"onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic",
|
||||
"deleteProfileTip": "Sure you want to delete the current profile?",
|
||||
"prueBlackMode": "Prue black mode",
|
||||
"pureBlackMode": "Pure black mode",
|
||||
"keepAliveIntervalDesc": "Tcp keep alive interval",
|
||||
"entries": " entries",
|
||||
"local": "Local",
|
||||
@@ -247,7 +247,6 @@
|
||||
"stop": "Stop",
|
||||
"appDesc": "Processing app related settings",
|
||||
"vpnDesc": "Modify VPN related settings",
|
||||
"generalDesc": "Overwrite general settings",
|
||||
"dnsDesc": "Update DNS related settings",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
@@ -341,5 +340,57 @@
|
||||
"nullProxies": "No proxies",
|
||||
"copySuccess": "Copy success",
|
||||
"copyLink": "Copy link",
|
||||
"exportFile": "Export file"
|
||||
"exportFile": "Export file",
|
||||
"cacheCorrupt": "The cache is corrupt. Do you want to clear it?",
|
||||
"detectionTip": "Relying on third-party api is for reference only",
|
||||
"listen": "Listen",
|
||||
"keyExists": "The current key already exists",
|
||||
"valueExists": "The current value already exists",
|
||||
"undo": "undo",
|
||||
"redo": "redo",
|
||||
"none": "none",
|
||||
"basicConfig": "Basic configuration",
|
||||
"basicConfigDesc": "Modify the basic configuration globally",
|
||||
"selectedCountTitle": "{count} items have been selected",
|
||||
"addRule": "Add rule",
|
||||
"ruleProviderEmptyTip": "Rule provider cannot be empty",
|
||||
"ruleName": "Rule name",
|
||||
"content": "Content",
|
||||
"contentEmptyTip": "Content cannot be empty",
|
||||
"subRule": "Sub rule",
|
||||
"subRuleEmptyTip": "Sub rule content cannot be empty",
|
||||
"ruleTarget": "Rule target",
|
||||
"ruleTargetEmptyTip": "Rule target cannot be empty",
|
||||
"sourceIp": "Source IP",
|
||||
"noResolve": "No resolve IP",
|
||||
"getOriginRules": "Get original rules",
|
||||
"overrideOriginRules": "Override the original rule",
|
||||
"addedOriginRules": "Attach on the original rules",
|
||||
"enableOverride": "Enable override",
|
||||
"deleteRuleTip": "Are you sure you want to delete the selected rule?",
|
||||
"saveChanges": "Do you want to save the changes?",
|
||||
"generalDesc": "Modify general settings",
|
||||
"findProcessModeDesc": "There is a certain performance loss after opening",
|
||||
"tabAnimationDesc": "Effective only in mobile view",
|
||||
"saveTip": "Are you sure you want to save?",
|
||||
"deleteColorTip": "Are you sure you want to delete the current color?",
|
||||
"colorExists": "Current color already exists",
|
||||
"colorSchemes": "Color schemes",
|
||||
"palette": "Palette",
|
||||
"tonalSpotScheme": "TonalSpot",
|
||||
"fidelityScheme": "Fidelity",
|
||||
"monochromeScheme": "Monochrome",
|
||||
"neutralScheme": "Neutral",
|
||||
"vibrantScheme": "Vibrant",
|
||||
"expressiveScheme": "Expressive",
|
||||
"contentScheme": "Content",
|
||||
"rainbowScheme": "Rainbow",
|
||||
"fruitSaladScheme": "FruitSalad",
|
||||
"developerMode": "Developer mode",
|
||||
"developerModeEnableTip": "Developer mode is enabled.",
|
||||
"messageTest": "Message test",
|
||||
"messageTestTip": "This is a message.",
|
||||
"crashTest": "Crash test",
|
||||
"clearData": "Clear Data",
|
||||
"textScale": "Text Scaling"
|
||||
}
|
||||
397
arb/intl_ja.arb
Normal file
397
arb/intl_ja.arb
Normal file
@@ -0,0 +1,397 @@
|
||||
{
|
||||
"rule": "ルール",
|
||||
"global": "グローバル",
|
||||
"direct": "ダイレクト",
|
||||
"dashboard": "ダッシュボード",
|
||||
"proxies": "プロキシ",
|
||||
"profile": "プロファイル",
|
||||
"profiles": "プロファイル一覧",
|
||||
"tools": "ツール",
|
||||
"logs": "ログ",
|
||||
"logsDesc": "ログキャプチャ記録",
|
||||
"resources": "リソース",
|
||||
"resourcesDesc": "外部リソース関連情報",
|
||||
"trafficUsage": "トラフィック使用量",
|
||||
"coreInfo": "コア情報",
|
||||
"nullCoreInfoDesc": "コア情報を取得できません",
|
||||
"networkSpeed": "ネットワーク速度",
|
||||
"outboundMode": "アウトバウンドモード",
|
||||
"networkDetection": "ネットワーク検出",
|
||||
"upload": "アップロード",
|
||||
"download": "ダウンロード",
|
||||
"noProxy": "プロキシなし",
|
||||
"noProxyDesc": "プロファイルを作成するか、有効なプロファイルを追加してください",
|
||||
"nullProfileDesc": "プロファイルがありません。追加してください",
|
||||
"nullLogsDesc": "ログがありません",
|
||||
"settings": "設定",
|
||||
"language": "言語",
|
||||
"defaultText": "デフォルト",
|
||||
"more": "詳細",
|
||||
"other": "その他",
|
||||
"about": "について",
|
||||
"en": "英語",
|
||||
"ja": "日本語",
|
||||
"ru": "ロシア語",
|
||||
"zh_CN": "簡体字中国語",
|
||||
"theme": "テーマ",
|
||||
"themeDesc": "ダークモードの設定、色の調整",
|
||||
"override": "上書き",
|
||||
"overrideDesc": "プロキシ関連設定を上書き",
|
||||
"allowLan": "LANを許可",
|
||||
"allowLanDesc": "LAN経由でのプロキシアクセスを許可",
|
||||
"tun": "TUN",
|
||||
"tunDesc": "管理者モードでのみ有効",
|
||||
"minimizeOnExit": "終了時に最小化",
|
||||
"minimizeOnExitDesc": "システムの終了イベントを変更",
|
||||
"autoLaunch": "自動起動",
|
||||
"autoLaunchDesc": "システムの自動起動に従う",
|
||||
"silentLaunch": "バックグラウンド起動",
|
||||
"silentLaunchDesc": "バックグラウンドで起動",
|
||||
"autoRun": "自動実行",
|
||||
"autoRunDesc": "アプリ起動時に自動実行",
|
||||
"logcat": "ログキャット",
|
||||
"logcatDesc": "無効化するとログエントリを非表示",
|
||||
"autoCheckUpdate": "自動更新チェック",
|
||||
"autoCheckUpdateDesc": "起動時に更新を自動チェック",
|
||||
"accessControl": "アクセス制御",
|
||||
"accessControlDesc": "アプリケーションのプロキシアクセスを設定",
|
||||
"application": "アプリケーション",
|
||||
"applicationDesc": "アプリ関連設定を変更",
|
||||
"edit": "編集",
|
||||
"confirm": "確認",
|
||||
"update": "更新",
|
||||
"add": "追加",
|
||||
"save": "保存",
|
||||
"delete": "削除",
|
||||
"years": "年",
|
||||
"months": "月",
|
||||
"hours": "時間",
|
||||
"days": "日",
|
||||
"minutes": "分",
|
||||
"seconds": "秒",
|
||||
"ago": "前",
|
||||
"just": "たった今",
|
||||
"qrcode": "QRコード",
|
||||
"qrcodeDesc": "QRコードをスキャンしてプロファイルを取得",
|
||||
"url": "URL",
|
||||
"urlDesc": "URL経由でプロファイルを取得",
|
||||
"file": "ファイル",
|
||||
"fileDesc": "プロファイルを直接アップロード",
|
||||
"name": "名前",
|
||||
"profileNameNullValidationDesc": "プロファイル名を入力してください",
|
||||
"profileUrlNullValidationDesc": "プロファイルURLを入力してください",
|
||||
"profileUrlInvalidValidationDesc": "有効なプロファイルURLを入力してください",
|
||||
"autoUpdate": "自動更新",
|
||||
"autoUpdateInterval": "自動更新間隔(分)",
|
||||
"profileAutoUpdateIntervalNullValidationDesc": "自動更新間隔を入力してください",
|
||||
"profileAutoUpdateIntervalInvalidValidationDesc": "有効な間隔形式を入力してください",
|
||||
"themeMode": "テーマモード",
|
||||
"themeColor": "テーマカラー",
|
||||
"preview": "プレビュー",
|
||||
"auto": "自動",
|
||||
"light": "ライト",
|
||||
"dark": "ダーク",
|
||||
"importFromURL": "URLからインポート",
|
||||
"submit": "送信",
|
||||
"doYouWantToPass": "通過させますか?",
|
||||
"create": "作成",
|
||||
"defaultSort": "デフォルト順",
|
||||
"delaySort": "遅延順",
|
||||
"nameSort": "名前順",
|
||||
"pleaseUploadFile": "ファイルをアップロードしてください",
|
||||
"pleaseUploadValidQrcode": "有効なQRコードをアップロードしてください",
|
||||
"blacklistMode": "ブラックリストモード",
|
||||
"whitelistMode": "ホワイトリストモード",
|
||||
"filterSystemApp": "システムアプリを除外",
|
||||
"cancelFilterSystemApp": "システムアプリの除外を解除",
|
||||
"selectAll": "すべて選択",
|
||||
"cancelSelectAll": "全選択解除",
|
||||
"appAccessControl": "アプリアクセス制御",
|
||||
"accessControlAllowDesc": "選択したアプリのみVPNを許可",
|
||||
"accessControlNotAllowDesc": "選択したアプリをVPNから除外",
|
||||
"selected": "選択済み",
|
||||
"unableToUpdateCurrentProfileDesc": "現在のプロファイルを更新できません",
|
||||
"noMoreInfoDesc": "追加情報なし",
|
||||
"profileParseErrorDesc": "プロファイル解析エラー",
|
||||
"proxyPort": "プロキシポート",
|
||||
"proxyPortDesc": "Clashのリスニングポートを設定",
|
||||
"port": "ポート",
|
||||
"logLevel": "ログレベル",
|
||||
"show": "表示",
|
||||
"exit": "終了",
|
||||
"systemProxy": "システムプロキシ",
|
||||
"project": "プロジェクト",
|
||||
"core": "コア",
|
||||
"tabAnimation": "タブアニメーション",
|
||||
"desc": "ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。",
|
||||
"startVpn": "VPNを開始中...",
|
||||
"stopVpn": "VPNを停止中...",
|
||||
"discovery": "新しいバージョンを発見",
|
||||
"compatible": "互換モード",
|
||||
"compatibleDesc": "有効化すると一部機能を失いますが、Clashの完全サポートを獲得",
|
||||
"notSelectedTip": "現在のプロキシグループは選択できません",
|
||||
"tip": "ヒント",
|
||||
"backupAndRecovery": "バックアップと復元",
|
||||
"backupAndRecoveryDesc": "WebDAVまたはファイルでデータを同期",
|
||||
"account": "アカウント",
|
||||
"backup": "バックアップ",
|
||||
"recovery": "復元",
|
||||
"recoveryProfiles": "プロファイルのみ復元",
|
||||
"recoveryAll": "全データ復元",
|
||||
"recoverySuccess": "復元成功",
|
||||
"backupSuccess": "バックアップ成功",
|
||||
"noInfo": "情報なし",
|
||||
"pleaseBindWebDAV": "WebDAVをバインドしてください",
|
||||
"bind": "バインド",
|
||||
"connectivity": "接続性:",
|
||||
"webDAVConfiguration": "WebDAV設定",
|
||||
"address": "アドレス",
|
||||
"addressHelp": "WebDAVサーバーアドレス",
|
||||
"addressTip": "有効なWebDAVアドレスを入力",
|
||||
"password": "パスワード",
|
||||
"passwordTip": "パスワードは必須です",
|
||||
"accountTip": "アカウントは必須です",
|
||||
"checkUpdate": "更新を確認",
|
||||
"discoverNewVersion": "新バージョンを発見",
|
||||
"checkUpdateError": "アプリは最新版です",
|
||||
"goDownload": "ダウンロードへ",
|
||||
"unknown": "不明",
|
||||
"geoData": "地域データ",
|
||||
"externalResources": "外部リソース",
|
||||
"checking": "確認中...",
|
||||
"country": "国",
|
||||
"checkError": "確認エラー",
|
||||
"search": "検索",
|
||||
"allowBypass": "アプリがVPNをバイパスすることを許可",
|
||||
"allowBypassDesc": "有効化すると一部アプリがVPNをバイパス",
|
||||
"externalController": "外部コントローラー",
|
||||
"externalControllerDesc": "有効化するとClashコアをポート9090で制御可能",
|
||||
"ipv6Desc": "有効化するとIPv6トラフィックを受信可能",
|
||||
"app": "アプリ",
|
||||
"general": "一般",
|
||||
"vpnSystemProxyDesc": "HTTPプロキシをVpnServiceに接続",
|
||||
"systemProxyDesc": "HTTPプロキシをVpnServiceに接続",
|
||||
"unifiedDelay": "統一遅延",
|
||||
"unifiedDelayDesc": "ハンドシェイクなどの余分な遅延を削除",
|
||||
"tcpConcurrent": "TCP並列処理",
|
||||
"tcpConcurrentDesc": "TCP並列処理を許可",
|
||||
"geodataLoader": "Geo低メモリモード",
|
||||
"geodataLoaderDesc": "有効化するとGeo低メモリローダーを使用",
|
||||
"requests": "リクエスト",
|
||||
"requestsDesc": "最近のリクエスト記録を表示",
|
||||
"findProcessMode": "プロセス検出",
|
||||
"init": "初期化",
|
||||
"infiniteTime": "長期有効",
|
||||
"expirationTime": "有効期限",
|
||||
"connections": "接続",
|
||||
"connectionsDesc": "現在の接続データを表示",
|
||||
"nullRequestsDesc": "リクエストなし",
|
||||
"nullConnectionsDesc": "接続なし",
|
||||
"intranetIP": "イントラネットIP",
|
||||
"view": "表示",
|
||||
"cut": "切り取り",
|
||||
"copy": "コピー",
|
||||
"paste": "貼り付け",
|
||||
"testUrl": "URLテスト",
|
||||
"sync": "同期",
|
||||
"exclude": "最近のタスクから非表示",
|
||||
"excludeDesc": "アプリがバックグラウンド時に最近のタスクから非表示",
|
||||
"oneColumn": "1列",
|
||||
"twoColumns": "2列",
|
||||
"threeColumns": "3列",
|
||||
"fourColumns": "4列",
|
||||
"expand": "標準",
|
||||
"shrink": "縮小",
|
||||
"min": "最小化",
|
||||
"tab": "タブ",
|
||||
"list": "リスト",
|
||||
"delay": "遅延",
|
||||
"style": "スタイル",
|
||||
"size": "サイズ",
|
||||
"sort": "並び替え",
|
||||
"columns": "列",
|
||||
"proxiesSetting": "プロキシ設定",
|
||||
"proxyGroup": "プロキシグループ",
|
||||
"go": "移動",
|
||||
"externalLink": "外部リンク",
|
||||
"otherContributors": "その他の貢献者",
|
||||
"autoCloseConnections": "接続を自動閉じる",
|
||||
"autoCloseConnectionsDesc": "ノード変更後に接続を自動閉じる",
|
||||
"onlyStatisticsProxy": "プロキシのみ統計",
|
||||
"onlyStatisticsProxyDesc": "有効化するとプロキシトラフィックのみ統計",
|
||||
"deleteProfileTip": "現在のプロファイルを削除しますか?",
|
||||
"pureBlackMode": "純黒モード",
|
||||
"keepAliveIntervalDesc": "TCPキープアライブ間隔",
|
||||
"entries": " エントリ",
|
||||
"local": "ローカル",
|
||||
"remote": "リモート",
|
||||
"remoteBackupDesc": "WebDAVにデータをバックアップ",
|
||||
"remoteRecoveryDesc": "WebDAVからデータを復元",
|
||||
"localBackupDesc": "ローカルにデータをバックアップ",
|
||||
"localRecoveryDesc": "ファイルからデータを復元",
|
||||
"mode": "モード",
|
||||
"time": "時間",
|
||||
"source": "ソース",
|
||||
"allApps": "全アプリ",
|
||||
"onlyOtherApps": "サードパーティアプリのみ",
|
||||
"action": "アクション",
|
||||
"intelligentSelected": "インテリジェント選択",
|
||||
"clipboardImport": "クリップボードからインポート",
|
||||
"clipboardExport": "クリップボードにエクスポート",
|
||||
"layout": "レイアウト",
|
||||
"tight": "密",
|
||||
"standard": "標準",
|
||||
"loose": "疎",
|
||||
"profilesSort": "プロファイルの並び替え",
|
||||
"start": "開始",
|
||||
"stop": "停止",
|
||||
"appDesc": "アプリ関連設定の処理",
|
||||
"vpnDesc": "VPN関連設定の変更",
|
||||
"dnsDesc": "DNS関連設定の更新",
|
||||
"key": "キー",
|
||||
"value": "値",
|
||||
"notEmpty": "空欄不可",
|
||||
"hostsDesc": "ホストを追加",
|
||||
"vpnTip": "変更はVPN再起動後に有効",
|
||||
"vpnEnableDesc": "VpnService経由で全システムトラフィックをルーティング",
|
||||
"options": "オプション",
|
||||
"loopback": "ループバック解除ツール",
|
||||
"loopbackDesc": "UWPループバック解除用",
|
||||
"providers": "プロバイダー",
|
||||
"proxyProviders": "プロキシプロバイダー",
|
||||
"ruleProviders": "ルールプロバイダー",
|
||||
"overrideDns": "DNS上書き",
|
||||
"overrideDnsDesc": "有効化するとプロファイルのDNS設定を上書き",
|
||||
"status": "ステータス",
|
||||
"statusDesc": "無効時はシステムDNSを使用",
|
||||
"preferH3Desc": "DOHのHTTP/3を優先使用",
|
||||
"respectRules": "ルール尊重",
|
||||
"respectRulesDesc": "DNS接続がルールに従う(proxy-server-nameserverの設定が必要)",
|
||||
"dnsMode": "DNSモード",
|
||||
"fakeipRange": "Fakeip範囲",
|
||||
"fakeipFilter": "Fakeipフィルター",
|
||||
"defaultNameserver": "デフォルトネームサーバー",
|
||||
"defaultNameserverDesc": "DNSサーバーの解決用",
|
||||
"nameserver": "ネームサーバー",
|
||||
"nameserverDesc": "ドメイン解決用",
|
||||
"useHosts": "ホストを使用",
|
||||
"useSystemHosts": "システムホストを使用",
|
||||
"nameserverPolicy": "ネームサーバーポリシー",
|
||||
"nameserverPolicyDesc": "対応するネームサーバーポリシーを指定",
|
||||
"proxyNameserver": "プロキシネームサーバー",
|
||||
"proxyNameserverDesc": "プロキシノード解決用ドメイン",
|
||||
"fallback": "フォールバック",
|
||||
"fallbackDesc": "通常はオフショアDNSを使用",
|
||||
"fallbackFilter": "フォールバックフィルター",
|
||||
"geoipCode": "GeoIPコード",
|
||||
"ipcidr": "IPCIDR",
|
||||
"domain": "ドメイン",
|
||||
"reset": "リセット",
|
||||
"action_view": "表示/非表示",
|
||||
"action_start": "開始/停止",
|
||||
"action_mode": "モード切替",
|
||||
"action_proxy": "システムプロキシ",
|
||||
"action_tun": "TUN",
|
||||
"disclaimer": "免責事項",
|
||||
"disclaimerDesc": "本ソフトウェアは学習交流や科学研究などの非営利目的でのみ使用されます。商用利用は厳禁です。いかなる商用活動も本ソフトウェアとは無関係です。",
|
||||
"agree": "同意",
|
||||
"hotkeyManagement": "ホットキー管理",
|
||||
"hotkeyManagementDesc": "キーボードでアプリを制御",
|
||||
"pressKeyboard": "キーボードを押してください",
|
||||
"inputCorrectHotkey": "正しいホットキーを入力",
|
||||
"hotkeyConflict": "ホットキー競合",
|
||||
"remove": "削除",
|
||||
"noHotKey": "ホットキーなし",
|
||||
"noNetwork": "ネットワークなし",
|
||||
"ipv6InboundDesc": "IPv6インバウンドを許可",
|
||||
"exportLogs": "ログをエクスポート",
|
||||
"exportSuccess": "エクスポート成功",
|
||||
"iconStyle": "アイコンスタイル",
|
||||
"onlyIcon": "アイコンのみ",
|
||||
"noIcon": "なし",
|
||||
"stackMode": "スタックモード",
|
||||
"network": "ネットワーク",
|
||||
"networkDesc": "ネットワーク関連設定の変更",
|
||||
"bypassDomain": "バイパスドメイン",
|
||||
"bypassDomainDesc": "システムプロキシ有効時のみ適用",
|
||||
"resetTip": "リセットを確定",
|
||||
"regExp": "正規表現",
|
||||
"icon": "アイコン",
|
||||
"iconConfiguration": "アイコン設定",
|
||||
"noData": "データなし",
|
||||
"adminAutoLaunch": "管理者自動起動",
|
||||
"adminAutoLaunchDesc": "管理者モードで起動",
|
||||
"fontFamily": "フォントファミリー",
|
||||
"systemFont": "システムフォント",
|
||||
"toggle": "トグル",
|
||||
"system": "システム",
|
||||
"routeMode": "ルートモード",
|
||||
"routeMode_bypassPrivate": "プライベートルートをバイパス",
|
||||
"routeMode_config": "設定を使用",
|
||||
"routeAddress": "ルートアドレス",
|
||||
"routeAddressDesc": "ルートアドレスを設定",
|
||||
"pleaseInputAdminPassword": "管理者パスワードを入力",
|
||||
"copyEnvVar": "環境変数をコピー",
|
||||
"memoryInfo": "メモリ情報",
|
||||
"cancel": "キャンセル",
|
||||
"fileIsUpdate": "ファイルが変更されました。保存しますか?",
|
||||
"profileHasUpdate": "プロファイルが変更されました。自動更新を無効化しますか?",
|
||||
"hasCacheChange": "変更をキャッシュしますか?",
|
||||
"nullProxies": "プロキシなし",
|
||||
"copySuccess": "コピー成功",
|
||||
"copyLink": "リンクをコピー",
|
||||
"exportFile": "ファイルをエクスポート",
|
||||
"cacheCorrupt": "キャッシュが破損しています。クリアしますか?",
|
||||
"detectionTip": "サードパーティAPIに依存(参考値)",
|
||||
"listen": "リスン",
|
||||
"keyExists": "現在のキーは既に存在します",
|
||||
"valueExists": "現在の値は既に存在します",
|
||||
"undo": "元に戻す",
|
||||
"redo": "やり直す",
|
||||
"none": "なし",
|
||||
"basicConfig": "基本設定",
|
||||
"basicConfigDesc": "基本設定をグローバルに変更",
|
||||
"selectedCountTitle": "{count} 項目が選択されています",
|
||||
"addRule": "ルールを追加",
|
||||
"ruleProviderEmptyTip": "ルールプロバイダーは必須です",
|
||||
"ruleName": "ルール名",
|
||||
"content": "内容",
|
||||
"contentEmptyTip": "内容は必須です",
|
||||
"subRule": "サブルール",
|
||||
"subRuleEmptyTip": "サブルールの内容は必須です",
|
||||
"ruleTarget": "ルール対象",
|
||||
"ruleTargetEmptyTip": "ルール対象は必須です",
|
||||
"sourceIp": "送信元IP",
|
||||
"noResolve": "IPを解決しない",
|
||||
"getOriginRules": "元のルールを取得",
|
||||
"overrideOriginRules": "元のルールを上書き",
|
||||
"addedOriginRules": "元のルールに追加",
|
||||
"enableOverride": "上書きを有効化",
|
||||
"deleteRuleTip": "選択したルールを削除しますか?",
|
||||
"saveChanges": "変更を保存しますか?",
|
||||
"generalDesc": "一般設定を変更",
|
||||
"findProcessModeDesc": "有効化するとパフォーマンスが若干低下します",
|
||||
"tabAnimationDesc": "モバイル表示でのみ有効",
|
||||
"saveTip": "保存してもよろしいですか?",
|
||||
"deleteColorTip": "現在の色を削除しますか?",
|
||||
"colorExists": "この色は既に存在します",
|
||||
"colorSchemes": "カラースキーム",
|
||||
"palette": "パレット",
|
||||
"tonalSpotScheme": "トーンスポット",
|
||||
"fidelityScheme": "ハイファイデリティー",
|
||||
"monochromeScheme": "モノクローム",
|
||||
"neutralScheme": "ニュートラル",
|
||||
"vibrantScheme": "ビブラント",
|
||||
"expressiveScheme": "エクスプレッシブ",
|
||||
"contentScheme": "コンテンツテーマ",
|
||||
"rainbowScheme": "レインボー",
|
||||
"fruitSaladScheme": "フルーツサラダ",
|
||||
"developerMode": "デベロッパーモード",
|
||||
"developerModeEnableTip": "デベロッパーモードが有効になりました。",
|
||||
"messageTest": "メッセージテスト",
|
||||
"messageTestTip": "これはメッセージです。",
|
||||
"crashTest": "クラッシュテスト",
|
||||
"clearData": "データを消去",
|
||||
"zoom": "ズーム",
|
||||
"textScale": "テキストスケーリング"
|
||||
}
|
||||
397
arb/intl_ru.arb
Normal file
397
arb/intl_ru.arb
Normal file
@@ -0,0 +1,397 @@
|
||||
{
|
||||
"rule": "Правило",
|
||||
"global": "Глобальный",
|
||||
"direct": "Прямой",
|
||||
"dashboard": "Панель управления",
|
||||
"proxies": "Прокси",
|
||||
"profile": "Профиль",
|
||||
"profiles": "Профили",
|
||||
"tools": "Инструменты",
|
||||
"logs": "Логи",
|
||||
"logsDesc": "Записи захвата логов",
|
||||
"resources": "Ресурсы",
|
||||
"resourcesDesc": "Информация, связанная с внешними ресурсами",
|
||||
"trafficUsage": "Использование трафика",
|
||||
"coreInfo": "Информация о ядре",
|
||||
"nullCoreInfoDesc": "Не удалось получить информацию о ядре",
|
||||
"networkSpeed": "Скорость сети",
|
||||
"outboundMode": "Режим исходящего трафика",
|
||||
"networkDetection": "Обнаружение сети",
|
||||
"upload": "Загрузка",
|
||||
"download": "Скачивание",
|
||||
"noProxy": "Нет прокси",
|
||||
"noProxyDesc": "Пожалуйста, создайте профиль или добавьте действительный профиль",
|
||||
"nullProfileDesc": "Нет профиля, пожалуйста, добавьте профиль",
|
||||
"nullLogsDesc": "Нет логов",
|
||||
"settings": "Настройки",
|
||||
"language": "Язык",
|
||||
"defaultText": "По умолчанию",
|
||||
"more": "Еще",
|
||||
"other": "Другое",
|
||||
"about": "О программе",
|
||||
"en": "Английский",
|
||||
"ja": "Японский",
|
||||
"ru": "Русский",
|
||||
"zh_CN": "Упрощенный китайский",
|
||||
"theme": "Тема",
|
||||
"themeDesc": "Установить темный режим, настроить цвет",
|
||||
"override": "Переопределить",
|
||||
"overrideDesc": "Переопределить конфигурацию, связанную с прокси",
|
||||
"allowLan": "Разрешить LAN",
|
||||
"allowLanDesc": "Разрешить доступ к прокси через локальную сеть",
|
||||
"tun": "TUN",
|
||||
"tunDesc": "действительно только в режиме администратора",
|
||||
"minimizeOnExit": "Свернуть при выходе",
|
||||
"minimizeOnExitDesc": "Изменить стандартное событие выхода из системы",
|
||||
"autoLaunch": "Автозапуск",
|
||||
"autoLaunchDesc": "Следовать автозапуску системы",
|
||||
"silentLaunch": "Тихий запуск",
|
||||
"silentLaunchDesc": "Запуск в фоновом режиме",
|
||||
"autoRun": "Автозапуск",
|
||||
"autoRunDesc": "Автоматический запуск при открытии приложения",
|
||||
"logcat": "Logcat",
|
||||
"logcatDesc": "Отключение скроет запись логов",
|
||||
"autoCheckUpdate": "Автопроверка обновлений",
|
||||
"autoCheckUpdateDesc": "Автоматически проверять обновления при запуске приложения",
|
||||
"accessControl": "Контроль доступа",
|
||||
"accessControlDesc": "Настройка доступа приложений к прокси",
|
||||
"application": "Приложение",
|
||||
"applicationDesc": "Изменение настроек, связанных с приложением",
|
||||
"edit": "Редактировать",
|
||||
"confirm": "Подтвердить",
|
||||
"update": "Обновить",
|
||||
"add": "Добавить",
|
||||
"save": "Сохранить",
|
||||
"delete": "Удалить",
|
||||
"years": "Лет",
|
||||
"months": "Месяцев",
|
||||
"hours": "Часов",
|
||||
"days": "Дней",
|
||||
"minutes": "Минут",
|
||||
"seconds": "Секунд",
|
||||
"ago": " назад",
|
||||
"just": "Только что",
|
||||
"qrcode": "QR-код",
|
||||
"qrcodeDesc": "Сканируйте QR-код для получения профиля",
|
||||
"url": "URL",
|
||||
"urlDesc": "Получить профиль через URL",
|
||||
"file": "Файл",
|
||||
"fileDesc": "Прямая загрузка профиля",
|
||||
"name": "Имя",
|
||||
"profileNameNullValidationDesc": "Пожалуйста, введите имя профиля",
|
||||
"profileUrlNullValidationDesc": "Пожалуйста, введите URL профиля",
|
||||
"profileUrlInvalidValidationDesc": "Пожалуйста, введите действительный URL профиля",
|
||||
"autoUpdate": "Автообновление",
|
||||
"autoUpdateInterval": "Интервал автообновления (минуты)",
|
||||
"profileAutoUpdateIntervalNullValidationDesc": "Пожалуйста, введите интервал времени для автообновления",
|
||||
"profileAutoUpdateIntervalInvalidValidationDesc": "Пожалуйста, введите действительный формат интервала времени",
|
||||
"themeMode": "Режим темы",
|
||||
"themeColor": "Цвет темы",
|
||||
"preview": "Предпросмотр",
|
||||
"auto": "Авто",
|
||||
"light": "Светлый",
|
||||
"dark": "Темный",
|
||||
"importFromURL": "Импорт из URL",
|
||||
"submit": "Отправить",
|
||||
"doYouWantToPass": "Вы хотите пропустить",
|
||||
"create": "Создать",
|
||||
"defaultSort": "Сортировка по умолчанию",
|
||||
"delaySort": "Сортировка по задержке",
|
||||
"nameSort": "Сортировка по имени",
|
||||
"pleaseUploadFile": "Пожалуйста, загрузите файл",
|
||||
"pleaseUploadValidQrcode": "Пожалуйста, загрузите действительный QR-код",
|
||||
"blacklistMode": "Режим черного списка",
|
||||
"whitelistMode": "Режим белого списка",
|
||||
"filterSystemApp": "Фильтровать системные приложения",
|
||||
"cancelFilterSystemApp": "Отменить фильтрацию системных приложений",
|
||||
"selectAll": "Выбрать все",
|
||||
"cancelSelectAll": "Отменить выбор всего",
|
||||
"appAccessControl": "Контроль доступа приложений",
|
||||
"accessControlAllowDesc": "Разрешить только выбранным приложениям доступ к VPN",
|
||||
"accessControlNotAllowDesc": "Выбранные приложения будут исключены из VPN",
|
||||
"selected": "Выбрано",
|
||||
"unableToUpdateCurrentProfileDesc": "невозможно обновить текущий профиль",
|
||||
"noMoreInfoDesc": "Нет дополнительной информации",
|
||||
"profileParseErrorDesc": "ошибка разбора профиля",
|
||||
"proxyPort": "Порт прокси",
|
||||
"proxyPortDesc": "Установить порт прослушивания Clash",
|
||||
"port": "Порт",
|
||||
"logLevel": "Уровень логов",
|
||||
"show": "Показать",
|
||||
"exit": "Выход",
|
||||
"systemProxy": "Системный прокси",
|
||||
"project": "Проект",
|
||||
"core": "Ядро",
|
||||
"tabAnimation": "Анимация вкладок",
|
||||
"desc": "Многоплатформенный прокси-клиент на основе ClashMeta, простой и удобный в использовании, с открытым исходным кодом и без рекламы.",
|
||||
"startVpn": "Запуск VPN...",
|
||||
"stopVpn": "Остановка VPN...",
|
||||
"discovery": "Обнаружена новая версия",
|
||||
"compatible": "Режим совместимости",
|
||||
"compatibleDesc": "Включение приведет к потере части функциональности приложения, но обеспечит полную поддержку Clash.",
|
||||
"notSelectedTip": "Текущая группа прокси не может быть выбрана.",
|
||||
"tip": "подсказка",
|
||||
"backupAndRecovery": "Резервное копирование и восстановление",
|
||||
"backupAndRecoveryDesc": "Синхронизация данных через WebDAV или файл",
|
||||
"account": "Аккаунт",
|
||||
"backup": "Резервное копирование",
|
||||
"recovery": "Восстановление",
|
||||
"recoveryProfiles": "Только восстановление профилей",
|
||||
"recoveryAll": "Восстановить все данные",
|
||||
"recoverySuccess": "Восстановление успешно",
|
||||
"backupSuccess": "Резервное копирование успешно",
|
||||
"noInfo": "Нет информации",
|
||||
"pleaseBindWebDAV": "Пожалуйста, привяжите WebDAV",
|
||||
"bind": "Привязать",
|
||||
"connectivity": "Связь:",
|
||||
"webDAVConfiguration": "Конфигурация WebDAV",
|
||||
"address": "Адрес",
|
||||
"addressHelp": "Адрес сервера WebDAV",
|
||||
"addressTip": "Пожалуйста, введите действительный адрес WebDAV",
|
||||
"password": "Пароль",
|
||||
"passwordTip": "Пароль не может быть пустым",
|
||||
"accountTip": "Аккаунт не может быть пустым",
|
||||
"checkUpdate": "Проверить обновления",
|
||||
"discoverNewVersion": "Обнаружена новая версия",
|
||||
"checkUpdateError": "Текущее приложение уже является последней версией",
|
||||
"goDownload": "Перейти к загрузке",
|
||||
"unknown": "Неизвестно",
|
||||
"geoData": "Геоданные",
|
||||
"externalResources": "Внешние ресурсы",
|
||||
"checking": "Проверка...",
|
||||
"country": "Страна",
|
||||
"checkError": "Ошибка проверки",
|
||||
"search": "Поиск",
|
||||
"allowBypass": "Разрешить приложениям обходить VPN",
|
||||
"allowBypassDesc": "Некоторые приложения могут обходить VPN при включении",
|
||||
"externalController": "Внешний контроллер",
|
||||
"externalControllerDesc": "При включении ядро Clash можно контролировать на порту 9090",
|
||||
"ipv6Desc": "При включении будет возможно получать IPv6 трафик",
|
||||
"app": "Приложение",
|
||||
"general": "Общие",
|
||||
"vpnSystemProxyDesc": "Прикрепить HTTP-прокси к VpnService",
|
||||
"systemProxyDesc": "Прикрепить HTTP-прокси к VpnService",
|
||||
"unifiedDelay": "Унифицированная задержка",
|
||||
"unifiedDelayDesc": "Убрать дополнительные задержки, такие как рукопожатие",
|
||||
"tcpConcurrent": "TCP параллелизм",
|
||||
"tcpConcurrentDesc": "Включение позволит использовать параллелизм TCP",
|
||||
"geodataLoader": "Режим низкого потребления памяти для геоданных",
|
||||
"geodataLoaderDesc": "Включение будет использовать загрузчик геоданных с низким потреблением памяти",
|
||||
"requests": "Запросы",
|
||||
"requestsDesc": "Просмотр последних записей запросов",
|
||||
"findProcessMode": "Режим поиска процесса",
|
||||
"init": "Инициализация",
|
||||
"infiniteTime": "Долгосрочное действие",
|
||||
"expirationTime": "Время истечения",
|
||||
"connections": "Соединения",
|
||||
"connectionsDesc": "Просмотр текущих данных о соединениях",
|
||||
"nullRequestsDesc": "Нет запросов",
|
||||
"nullConnectionsDesc": "Нет соединений",
|
||||
"intranetIP": "Внутренний IP",
|
||||
"view": "Просмотр",
|
||||
"cut": "Вырезать",
|
||||
"copy": "Копировать",
|
||||
"paste": "Вставить",
|
||||
"testUrl": "Тест URL",
|
||||
"sync": "Синхронизация",
|
||||
"exclude": "Скрыть из последних задач",
|
||||
"excludeDesc": "Когда приложение находится в фоновом режиме, оно скрыто из последних задач",
|
||||
"oneColumn": "Один столбец",
|
||||
"twoColumns": "Два столбца",
|
||||
"threeColumns": "Три столбца",
|
||||
"fourColumns": "Четыре столбца",
|
||||
"expand": "Стандартный",
|
||||
"shrink": "Сжать",
|
||||
"min": "Мин",
|
||||
"tab": "Вкладка",
|
||||
"list": "Список",
|
||||
"delay": "Задержка",
|
||||
"style": "Стиль",
|
||||
"size": "Размер",
|
||||
"sort": "Сортировка",
|
||||
"columns": "Столбцы",
|
||||
"proxiesSetting": "Настройка прокси",
|
||||
"proxyGroup": "Группа прокси",
|
||||
"go": "Перейти",
|
||||
"externalLink": "Внешняя ссылка",
|
||||
"otherContributors": "Другие участники",
|
||||
"autoCloseConnections": "Автоматическое закрытие соединений",
|
||||
"autoCloseConnectionsDesc": "Автоматически закрывать соединения после смены узла",
|
||||
"onlyStatisticsProxy": "Только статистика прокси",
|
||||
"onlyStatisticsProxyDesc": "При включении будет учитываться только трафик прокси",
|
||||
"deleteProfileTip": "Вы уверены, что хотите удалить текущий профиль?",
|
||||
"pureBlackMode": "Чисто черный режим",
|
||||
"keepAliveIntervalDesc": "Интервал поддержания TCP-соединения",
|
||||
"entries": " записей",
|
||||
"local": "Локальный",
|
||||
"remote": "Удаленный",
|
||||
"remoteBackupDesc": "Резервное копирование локальных данных на WebDAV",
|
||||
"remoteRecoveryDesc": "Восстановление данных с WebDAV",
|
||||
"localBackupDesc": "Резервное копирование локальных данных на локальный диск",
|
||||
"localRecoveryDesc": "Восстановление данных из файла",
|
||||
"mode": "Режим",
|
||||
"time": "Время",
|
||||
"source": "Источник",
|
||||
"allApps": "Все приложения",
|
||||
"onlyOtherApps": "Только сторонние приложения",
|
||||
"action": "Действие",
|
||||
"intelligentSelected": "Интеллектуальный выбор",
|
||||
"clipboardImport": "Импорт из буфера обмена",
|
||||
"clipboardExport": "Экспорт в буфер обмена",
|
||||
"layout": "Макет",
|
||||
"tight": "Плотный",
|
||||
"standard": "Стандартный",
|
||||
"loose": "Свободный",
|
||||
"profilesSort": "Сортировка профилей",
|
||||
"start": "Старт",
|
||||
"stop": "Стоп",
|
||||
"appDesc": "Обработка настроек, связанных с приложением",
|
||||
"vpnDesc": "Изменение настроек, связанных с VPN",
|
||||
"dnsDesc": "Обновление настроек, связанных с DNS",
|
||||
"key": "Ключ",
|
||||
"value": "Значение",
|
||||
"notEmpty": "Не может быть пустым",
|
||||
"hostsDesc": "Добавить Hosts",
|
||||
"vpnTip": "Изменения вступят в силу после перезапуска VPN",
|
||||
"vpnEnableDesc": "Автоматически направляет весь системный трафик через VpnService",
|
||||
"options": "Опции",
|
||||
"loopback": "Инструмент разблокировки Loopback",
|
||||
"loopbackDesc": "Используется для разблокировки Loopback UWP",
|
||||
"providers": "Провайдеры",
|
||||
"proxyProviders": "Провайдеры прокси",
|
||||
"ruleProviders": "Провайдеры правил",
|
||||
"overrideDns": "Переопределить DNS",
|
||||
"overrideDnsDesc": "Включение переопределит настройки DNS в профиле",
|
||||
"status": "Статус",
|
||||
"statusDesc": "Системный DNS будет использоваться при выключении",
|
||||
"preferH3Desc": "Приоритетное использование HTTP/3 для DOH",
|
||||
"respectRules": "Соблюдение правил",
|
||||
"respectRulesDesc": "DNS-соединение следует правилам, необходимо настроить proxy-server-nameserver",
|
||||
"dnsMode": "Режим DNS",
|
||||
"fakeipRange": "Диапазон Fakeip",
|
||||
"fakeipFilter": "Фильтр Fakeip",
|
||||
"defaultNameserver": "Сервер имен по умолчанию",
|
||||
"defaultNameserverDesc": "Для разрешения DNS-сервера",
|
||||
"nameserver": "Сервер имен",
|
||||
"nameserverDesc": "Для разрешения домена",
|
||||
"useHosts": "Использовать hosts",
|
||||
"useSystemHosts": "Использовать системные hosts",
|
||||
"nameserverPolicy": "Политика сервера имен",
|
||||
"nameserverPolicyDesc": "Указать соответствующую политику сервера имен",
|
||||
"proxyNameserver": "Прокси-сервер имен",
|
||||
"proxyNameserverDesc": "Домен для разрешения прокси-узлов",
|
||||
"fallback": "Резервный",
|
||||
"fallbackDesc": "Обычно используется оффшорный DNS",
|
||||
"fallbackFilter": "Фильтр резервного DNS",
|
||||
"geoipCode": "Код Geoip",
|
||||
"ipcidr": "IPCIDR",
|
||||
"domain": "Домен",
|
||||
"reset": "Сброс",
|
||||
"action_view": "Показать/Скрыть",
|
||||
"action_start": "Старт/Стоп",
|
||||
"action_mode": "Переключить режим",
|
||||
"action_proxy": "Системный прокси",
|
||||
"action_tun": "TUN",
|
||||
"disclaimer": "Отказ от ответственности",
|
||||
"disclaimerDesc": "Это программное обеспечение используется только в некоммерческих целях, таких как учебные обмены и научные исследования. Запрещено использовать это программное обеспечение в коммерческих целях. Любая коммерческая деятельность, если таковая имеется, не имеет отношения к этому программному обеспечению.",
|
||||
"agree": "Согласен",
|
||||
"hotkeyManagement": "Управление горячими клавишами",
|
||||
"hotkeyManagementDesc": "Использование клавиатуры для управления приложением",
|
||||
"pressKeyboard": "Пожалуйста, нажмите клавишу.",
|
||||
"inputCorrectHotkey": "Пожалуйста, введите правильную горячую клавишу",
|
||||
"hotkeyConflict": "Конфликт горячих клавиш",
|
||||
"remove": "Удалить",
|
||||
"noHotKey": "Нет горячей клавиши",
|
||||
"noNetwork": "Нет сети",
|
||||
"ipv6InboundDesc": "Разрешить входящий IPv6",
|
||||
"exportLogs": "Экспорт логов",
|
||||
"exportSuccess": "Экспорт успешен",
|
||||
"iconStyle": "Стиль иконки",
|
||||
"onlyIcon": "Только иконка",
|
||||
"noIcon": "Нет иконки",
|
||||
"stackMode": "Режим стека",
|
||||
"network": "Сеть",
|
||||
"networkDesc": "Изменение настроек, связанных с сетью",
|
||||
"bypassDomain": "Обход домена",
|
||||
"bypassDomainDesc": "Действует только при включенном системном прокси",
|
||||
"resetTip": "Убедитесь, что хотите сбросить",
|
||||
"regExp": "Регулярное выражение",
|
||||
"icon": "Иконка",
|
||||
"iconConfiguration": "Конфигурация иконки",
|
||||
"noData": "Нет данных",
|
||||
"adminAutoLaunch": "Автозапуск с правами администратора",
|
||||
"adminAutoLaunchDesc": "Запуск с правами администратора при загрузке системы",
|
||||
"fontFamily": "Семейство шрифтов",
|
||||
"systemFont": "Системный шрифт",
|
||||
"toggle": "Переключить",
|
||||
"system": "Система",
|
||||
"routeMode": "Режим маршрутизации",
|
||||
"routeMode_bypassPrivate": "Обход частных адресов маршрутизации",
|
||||
"routeMode_config": "Использовать конфигурацию",
|
||||
"routeAddress": "Адрес маршрутизации",
|
||||
"routeAddressDesc": "Настройка адреса прослушивания маршрутизации",
|
||||
"pleaseInputAdminPassword": "Пожалуйста, введите пароль администратора",
|
||||
"copyEnvVar": "Копирование переменных окружения",
|
||||
"memoryInfo": "Информация о памяти",
|
||||
"cancel": "Отмена",
|
||||
"fileIsUpdate": "Файл был изменен. Хотите сохранить изменения?",
|
||||
"profileHasUpdate": "Профиль был изменен. Хотите отключить автообновление?",
|
||||
"hasCacheChange": "Хотите сохранить изменения в кэше?",
|
||||
"nullProxies": "Нет прокси",
|
||||
"copySuccess": "Копирование успешно",
|
||||
"copyLink": "Копировать ссылку",
|
||||
"exportFile": "Экспорт файла",
|
||||
"cacheCorrupt": "Кэш поврежден. Хотите очистить его?",
|
||||
"detectionTip": "Опирается на сторонний API, только для справки",
|
||||
"listen": "Слушать",
|
||||
"keyExists": "Текущий ключ уже существует",
|
||||
"valueExists": "Текущее значение уже существует",
|
||||
"undo": "Отменить",
|
||||
"redo": "Повторить",
|
||||
"none": "Нет",
|
||||
"basicConfig": "Базовая конфигурация",
|
||||
"basicConfigDesc": "Глобальное изменение базовых настроек",
|
||||
"selectedCountTitle": "Выбрано {count} элементов",
|
||||
"addRule": "Добавить правило",
|
||||
"ruleProviderEmptyTip": "Поставщик правил не может быть пустым",
|
||||
"ruleName": "Название правила",
|
||||
"content": "Содержание",
|
||||
"contentEmptyTip": "Содержание не может быть пустым",
|
||||
"subRule": "Подправило",
|
||||
"subRuleEmptyTip": "Содержание подправила не может быть пустым",
|
||||
"ruleTarget": "Цель правила",
|
||||
"ruleTargetEmptyTip": "Цель правила не может быть пустой",
|
||||
"sourceIp": "Исходный IP",
|
||||
"noResolve": "Не разрешать IP",
|
||||
"getOriginRules": "Получить оригинальные правила",
|
||||
"overrideOriginRules": "Переопределить оригинальное правило",
|
||||
"addedOriginRules": "Добавить к оригинальным правилам",
|
||||
"enableOverride": "Включить переопределение",
|
||||
"deleteRuleTip": "Вы уверены, что хотите удалить выбранное правило?",
|
||||
"saveChanges": "Сохранить изменения?",
|
||||
"generalDesc": "Изменение общих настроек",
|
||||
"findProcessModeDesc": "При включении возможны небольшие потери производительности",
|
||||
"tabAnimationDesc": "Действительно только в мобильном виде",
|
||||
"saveTip": "Вы уверены, что хотите сохранить?",
|
||||
"deleteColorTip": "Удалить текущий цвет?",
|
||||
"colorExists": "Этот цвет уже существует",
|
||||
"colorSchemes": "Цветовые схемы",
|
||||
"palette": "Палитра",
|
||||
"tonalSpotScheme": "Тональный акцент",
|
||||
"fidelityScheme": "Точная передача",
|
||||
"monochromeScheme": "Монохром",
|
||||
"neutralScheme": "Нейтральные",
|
||||
"vibrantScheme": "Яркие",
|
||||
"expressiveScheme": "Экспрессивные",
|
||||
"contentScheme": "Контентная тема",
|
||||
"rainbowScheme": "Радужные",
|
||||
"fruitSaladScheme": "Фруктовый микс",
|
||||
"developerMode": "Режим разработчика",
|
||||
"developerModeEnableTip": "Режим разработчика активирован.",
|
||||
"messageTest": "Тестирование сообщения",
|
||||
"messageTestTip": "Это сообщение.",
|
||||
"crashTest": "Тест на сбои",
|
||||
"clearData": "Очистить данные",
|
||||
"zoom": "Масштаб",
|
||||
"textScale": "Масштабирование текста"
|
||||
}
|
||||
@@ -30,6 +30,8 @@
|
||||
"other": "其他",
|
||||
"about": "关于",
|
||||
"en": "英语",
|
||||
"ja": "日语",
|
||||
"ru": "俄语",
|
||||
"zh_CN": "中文简体",
|
||||
"theme": "主题",
|
||||
"themeDesc": "设置深色模式,调整色彩",
|
||||
@@ -121,7 +123,6 @@
|
||||
"project": "项目",
|
||||
"core": "内核",
|
||||
"tabAnimation": "选项卡动画",
|
||||
"tabAnimationDesc": "开启后,主页选项卡将添加切换动画",
|
||||
"desc": "基于ClashMeta的多平台代理客户端,简单易用,开源无广告。",
|
||||
"startVpn": "正在启动VPN...",
|
||||
"stopVpn": "正在停止VPN...",
|
||||
@@ -167,7 +168,7 @@
|
||||
"externalControllerDesc": "开启后将可以通过9090端口控制Clash内核",
|
||||
"ipv6Desc": "开启后将可以接收IPv6流量",
|
||||
"app": "应用",
|
||||
"general": "基础",
|
||||
"general": "常规",
|
||||
"vpnSystemProxyDesc": "为VpnService附加HTTP代理",
|
||||
"systemProxyDesc": "设置系统代理",
|
||||
"unifiedDelay": "统一延迟",
|
||||
@@ -179,7 +180,6 @@
|
||||
"requests": "请求",
|
||||
"requestsDesc": "查看最近请求记录",
|
||||
"findProcessMode": "查找进程",
|
||||
"findProcessModeDesc": "开启后存在闪退风险",
|
||||
"init": "初始化",
|
||||
"infiniteTime": "长期有效",
|
||||
"expirationTime": "到期时间",
|
||||
@@ -220,7 +220,7 @@
|
||||
"onlyStatisticsProxy": "仅统计代理",
|
||||
"onlyStatisticsProxyDesc": "开启后,将只统计代理流量",
|
||||
"deleteProfileTip": "确定要删除当前配置吗?",
|
||||
"prueBlackMode": "纯黑模式",
|
||||
"pureBlackMode": "纯黑模式",
|
||||
"keepAliveIntervalDesc": "TCP保持活动间隔",
|
||||
"entries": "个条目",
|
||||
"local": "本地",
|
||||
@@ -247,7 +247,6 @@
|
||||
"stop": "暂停",
|
||||
"appDesc": "处理应用相关设置",
|
||||
"vpnDesc": "修改VPN相关设置",
|
||||
"generalDesc": "覆写基础设置",
|
||||
"dnsDesc": "更新DNS相关设置",
|
||||
"key": "键",
|
||||
"value": "值",
|
||||
@@ -341,5 +340,58 @@
|
||||
"nullProxies": "暂无代理",
|
||||
"copySuccess": "复制成功",
|
||||
"copyLink": "复制链接",
|
||||
"exportFile": "导出文件"
|
||||
"exportFile": "导出文件",
|
||||
"cacheCorrupt": "缓存已损坏,是否清空?",
|
||||
"detectionTip": "依赖第三方api,仅供参考",
|
||||
"listen": "监听",
|
||||
"keyExists": "当前键已存在",
|
||||
"valueExists": "当前值已存在",
|
||||
"undo": "撤销",
|
||||
"redo": "重做",
|
||||
"none": "无",
|
||||
"basicConfig": "基本配置",
|
||||
"basicConfigDesc": "全局修改基本配置",
|
||||
"selectedCountTitle": "已选择 {count} 项",
|
||||
"addRule": "添加规则",
|
||||
"ruleProviderEmptyTip": "规则提供者不能为空",
|
||||
"ruleName": "规则名称",
|
||||
"content": "内容",
|
||||
"contentEmptyTip": "内容不能为空",
|
||||
"subRule": "子规则",
|
||||
"subRuleEmptyTip": "子规则内容不能为空",
|
||||
"ruleTarget": "规则目标",
|
||||
"ruleTargetEmptyTip": "规则目标不能为空",
|
||||
"sourceIp": "源IP",
|
||||
"noResolve": "不解析IP",
|
||||
"getOriginRules": "获取原始规则",
|
||||
"overrideOriginRules": "覆盖原始规则",
|
||||
"addedOriginRules": "附加到原始规则",
|
||||
"enableOverride": "启用覆写",
|
||||
"deleteRuleTip": "确定要删除选中的规则吗?",
|
||||
"saveChanges": "是否保存更改?",
|
||||
"generalDesc": "修改通用设置",
|
||||
"findProcessModeDesc": "开启后会有一定性能损耗",
|
||||
"tabAnimationDesc": "仅在移动视图中有效",
|
||||
"saveTip": "确定要保存吗?",
|
||||
"deleteColorTip": "确定删除当前颜色吗?",
|
||||
"colorExists": "该颜色已存在",
|
||||
"colorSchemes": "配色方案",
|
||||
"palette": "调色板",
|
||||
"tonalSpotScheme": "调性点缀",
|
||||
"fidelityScheme": "高保真",
|
||||
"monochromeScheme": "单色",
|
||||
"neutralScheme": "中性",
|
||||
"vibrantScheme": "活力",
|
||||
"expressiveScheme": "表现力",
|
||||
"contentScheme": "内容主题",
|
||||
"rainbowScheme": "彩虹",
|
||||
"fruitSaladScheme": "果缤纷",
|
||||
"developerMode": "开发者模式",
|
||||
"developerModeEnableTip": "开发者模式已启用。",
|
||||
"messageTest": "消息测试",
|
||||
"messageTestTip": "这是一条消息。",
|
||||
"crashTest": "崩溃测试",
|
||||
"clearData": "清除数据",
|
||||
"zoom": "缩放",
|
||||
"textScale": "文本缩放"
|
||||
}
|
||||
BIN
assets/fonts/JetBrainsMono-Regular.ttf
Normal file
BIN
assets/fonts/JetBrainsMono-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
@@ -5,6 +5,7 @@ targets:
|
||||
options:
|
||||
build_extensions:
|
||||
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
|
||||
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
|
||||
freezed:
|
||||
options:
|
||||
build_extensions:
|
||||
|
||||
Submodule core/Clash.Meta updated: 0c03d8e4b4...88a1848dfb
@@ -22,99 +22,99 @@ func (result ActionResult) Json() ([]byte, error) {
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (action Action) wrapMessage(data interface{}) []byte {
|
||||
sendAction := ActionResult{
|
||||
func (action Action) getResult(data interface{}) []byte {
|
||||
resultAction := ActionResult{
|
||||
Id: action.Id,
|
||||
Method: action.Method,
|
||||
Data: data,
|
||||
}
|
||||
res, _ := sendAction.Json()
|
||||
res, _ := resultAction.Json()
|
||||
return res
|
||||
}
|
||||
|
||||
func handleAction(action *Action, send func([]byte)) {
|
||||
func handleAction(action *Action, result func(data interface{})) {
|
||||
switch action.Method {
|
||||
case initClashMethod:
|
||||
data := action.Data.(string)
|
||||
send(action.wrapMessage(handleInitClash(data)))
|
||||
paramsString := action.Data.(string)
|
||||
result(handleInitClash(paramsString))
|
||||
return
|
||||
case getIsInitMethod:
|
||||
send(action.wrapMessage(handleGetIsInit()))
|
||||
result(handleGetIsInit())
|
||||
return
|
||||
case forceGcMethod:
|
||||
handleForceGc()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case shutdownMethod:
|
||||
send(action.wrapMessage(handleShutdown()))
|
||||
result(handleShutdown())
|
||||
return
|
||||
case validateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
send(action.wrapMessage(handleValidateConfig(data)))
|
||||
result(handleValidateConfig(data))
|
||||
return
|
||||
case updateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
send(action.wrapMessage(handleUpdateConfig(data)))
|
||||
result(handleUpdateConfig(data))
|
||||
return
|
||||
case getProxiesMethod:
|
||||
send(action.wrapMessage(handleGetProxies()))
|
||||
result(handleGetProxies())
|
||||
return
|
||||
case changeProxyMethod:
|
||||
data := action.Data.(string)
|
||||
handleChangeProxy(data, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getTrafficMethod:
|
||||
send(action.wrapMessage(handleGetTraffic()))
|
||||
result(handleGetTraffic())
|
||||
return
|
||||
case getTotalTrafficMethod:
|
||||
send(action.wrapMessage(handleGetTotalTraffic()))
|
||||
result(handleGetTotalTraffic())
|
||||
return
|
||||
case resetTrafficMethod:
|
||||
handleResetTraffic()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case asyncTestDelayMethod:
|
||||
data := action.Data.(string)
|
||||
handleAsyncTestDelay(data, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getConnectionsMethod:
|
||||
send(action.wrapMessage(handleGetConnections()))
|
||||
result(handleGetConnections())
|
||||
return
|
||||
case closeConnectionsMethod:
|
||||
send(action.wrapMessage(handleCloseConnections()))
|
||||
result(handleCloseConnections())
|
||||
return
|
||||
case closeConnectionMethod:
|
||||
id := action.Data.(string)
|
||||
send(action.wrapMessage(handleCloseConnection(id)))
|
||||
result(handleCloseConnection(id))
|
||||
return
|
||||
case getExternalProvidersMethod:
|
||||
send(action.wrapMessage(handleGetExternalProviders()))
|
||||
result(handleGetExternalProviders())
|
||||
return
|
||||
case getExternalProviderMethod:
|
||||
externalProviderName := action.Data.(string)
|
||||
send(action.wrapMessage(handleGetExternalProvider(externalProviderName)))
|
||||
result(handleGetExternalProvider(externalProviderName))
|
||||
case updateGeoDataMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
send(action.wrapMessage(err.Error()))
|
||||
result(err.Error())
|
||||
return
|
||||
}
|
||||
geoType := params["geo-type"]
|
||||
geoName := params["geo-name"]
|
||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case updateExternalProviderMethod:
|
||||
providerName := action.Data.(string)
|
||||
handleUpdateExternalProvider(providerName, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case sideLoadExternalProviderMethod:
|
||||
@@ -122,46 +122,59 @@ func handleAction(action *Action, send func([]byte)) {
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
send(action.wrapMessage(err.Error()))
|
||||
result(err.Error())
|
||||
return
|
||||
}
|
||||
providerName := params["providerName"]
|
||||
data := params["data"]
|
||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case startLogMethod:
|
||||
handleStartLog()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case stopLogMethod:
|
||||
handleStopLog()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case startListenerMethod:
|
||||
send(action.wrapMessage(handleStartListener()))
|
||||
result(handleStartListener())
|
||||
return
|
||||
case stopListenerMethod:
|
||||
send(action.wrapMessage(handleStopListener()))
|
||||
result(handleStopListener())
|
||||
return
|
||||
case getCountryCodeMethod:
|
||||
ip := action.Data.(string)
|
||||
handleGetCountryCode(ip, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getMemoryMethod:
|
||||
handleGetMemory(func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getProfileMethod:
|
||||
profileId := action.Data.(string)
|
||||
handleGetMemory(func(value string) {
|
||||
result(handleGetProfile(profileId))
|
||||
})
|
||||
return
|
||||
case setStateMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetState(data)
|
||||
result(true)
|
||||
case crashMethod:
|
||||
result(true)
|
||||
handleCrash()
|
||||
default:
|
||||
handle := nextHandle(action, send)
|
||||
handle := nextHandle(action, result)
|
||||
if handle {
|
||||
return
|
||||
} else {
|
||||
send(action.wrapMessage(action.DefaultValue))
|
||||
result(action.DefaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
77
core/android_bride.go
Normal file
77
core/android_bride.go
Normal file
@@ -0,0 +1,77 @@
|
||||
//go:build android && cgo
|
||||
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef void (*release_object_func)(void *obj);
|
||||
|
||||
typedef void (*protect_func)(void *tun_interface, int fd);
|
||||
|
||||
typedef const char* (*resolve_process_func)(void *tun_interface, int protocol, const char *source, const char *target, int uid);
|
||||
|
||||
static void protect(protect_func fn, void *tun_interface, int fd) {
|
||||
if (fn) {
|
||||
fn(tun_interface, fd);
|
||||
}
|
||||
}
|
||||
|
||||
static const char* resolve_process(resolve_process_func fn, void *tun_interface, int protocol, const char *source, const char *target, int uid) {
|
||||
if (fn) {
|
||||
return fn(tun_interface, protocol, source, target, uid);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static void release_object(release_object_func fn, void *obj) {
|
||||
if (fn) {
|
||||
return fn(obj);
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
globalCallbacks struct {
|
||||
releaseObjectFunc C.release_object_func
|
||||
protectFunc C.protect_func
|
||||
resolveProcessFunc C.resolve_process_func
|
||||
}
|
||||
)
|
||||
|
||||
func protect(callback unsafe.Pointer, fd int) {
|
||||
if globalCallbacks.protectFunc != nil {
|
||||
C.protect(globalCallbacks.protectFunc, callback, C.int(fd))
|
||||
}
|
||||
}
|
||||
|
||||
func resolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
|
||||
if globalCallbacks.resolveProcessFunc == nil {
|
||||
return ""
|
||||
}
|
||||
s := C.CString(source)
|
||||
defer C.free(unsafe.Pointer(s))
|
||||
t := C.CString(target)
|
||||
defer C.free(unsafe.Pointer(t))
|
||||
res := C.resolve_process(globalCallbacks.resolveProcessFunc, callback, C.int(protocol), s, t, C.int(uid))
|
||||
defer C.free(unsafe.Pointer(res))
|
||||
return C.GoString(res)
|
||||
}
|
||||
|
||||
func releaseObject(callback unsafe.Pointer) {
|
||||
if globalCallbacks.releaseObjectFunc == nil {
|
||||
return
|
||||
}
|
||||
C.release_object(globalCallbacks.releaseObjectFunc, callback)
|
||||
}
|
||||
|
||||
//export registerCallbacks
|
||||
func registerCallbacks(markSocketFunc C.protect_func, resolveProcessFunc C.resolve_process_func, releaseObjectFunc C.release_object_func) {
|
||||
globalCallbacks.protectFunc = markSocketFunc
|
||||
globalCallbacks.resolveProcessFunc = resolveProcessFunc
|
||||
globalCallbacks.releaseObjectFunc = releaseObjectFunc
|
||||
}
|
||||
@@ -28,10 +28,23 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
func splitByMultipleSeparators(s string) interface{} {
|
||||
isSeparator := func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == ';'
|
||||
}
|
||||
|
||||
parts := strings.FieldsFunc(s, isSeparator)
|
||||
if len(parts) > 1 {
|
||||
return parts
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
version = 0
|
||||
isRunning = false
|
||||
runLock sync.Mutex
|
||||
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"}
|
||||
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
|
||||
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
||||
)
|
||||
|
||||
@@ -65,7 +78,6 @@ func getRawConfigWithId(id string) *config.RawConfig {
|
||||
path := getProfilePath(id)
|
||||
bytes, err := readFile(path)
|
||||
if err != nil {
|
||||
log.Errorln("profile is not exist")
|
||||
return config.DefaultRawConfig()
|
||||
}
|
||||
prof, err := config.UnmarshalRawConfig(bytes)
|
||||
@@ -160,9 +172,19 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
|
||||
return prof
|
||||
}
|
||||
|
||||
func genHosts(hosts, patchHosts map[string]any) {
|
||||
func attachHosts(hosts, patchHosts map[string]any) {
|
||||
for k, v := range patchHosts {
|
||||
hosts[k] = v
|
||||
if str, ok := v.(string); ok {
|
||||
hosts[k] = splitByMultipleSeparators(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePatchDns(dns config.RawDNS) {
|
||||
for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() {
|
||||
if str, ok := pair.Value.(string); ok {
|
||||
dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,26 +195,25 @@ func trimArr(arr []string) (r []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func overrideRules(rules *[]string) {
|
||||
var target = ""
|
||||
for _, line := range *rules {
|
||||
func overrideRules(rules, patchRules []string) []string {
|
||||
target := ""
|
||||
for _, line := range rules {
|
||||
rule := trimArr(strings.Split(line, ","))
|
||||
l := len(rule)
|
||||
if l != 2 {
|
||||
return
|
||||
if len(rule) != 2 {
|
||||
continue
|
||||
}
|
||||
if strings.ToUpper(rule[0]) == "MATCH" {
|
||||
if strings.EqualFold(rule[0], "MATCH") {
|
||||
target = rule[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if target == "" {
|
||||
return
|
||||
return rules
|
||||
}
|
||||
var rulesExt = lo.Map(ips, func(ip string, index int) string {
|
||||
return fmt.Sprintf("DOMAIN %s %s", ip, target)
|
||||
rulesExt := lo.Map(ips, func(ip string, _ int) string {
|
||||
return fmt.Sprintf("DOMAIN,%s,%s", ip, target)
|
||||
})
|
||||
*rules = append(rulesExt, *rules...)
|
||||
return append(append(rulesExt, patchRules...), rules...)
|
||||
}
|
||||
|
||||
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
|
||||
@@ -215,6 +236,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
targetConfig.Tun.Device = patchConfig.Tun.Device
|
||||
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
||||
targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
||||
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
|
||||
targetConfig.GeodataLoader = patchConfig.GeodataLoader
|
||||
targetConfig.Profile.StoreSelected = false
|
||||
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
||||
@@ -225,15 +247,20 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
for idx := range targetConfig.ProxyGroup {
|
||||
targetConfig.ProxyGroup[idx]["url"] = ""
|
||||
}
|
||||
genHosts(targetConfig.Hosts, patchConfig.Hosts)
|
||||
attachHosts(targetConfig.Hosts, patchConfig.Hosts)
|
||||
if configParams.OverrideDns {
|
||||
updatePatchDns(patchConfig.DNS)
|
||||
targetConfig.DNS = patchConfig.DNS
|
||||
} else {
|
||||
if targetConfig.DNS.Enable == false {
|
||||
targetConfig.DNS.Enable = true
|
||||
}
|
||||
}
|
||||
overrideRules(&targetConfig.Rule)
|
||||
if configParams.OverrideRule {
|
||||
targetConfig.Rule = overrideRules(patchConfig.Rule, []string{})
|
||||
} else {
|
||||
targetConfig.Rule = overrideRules(targetConfig.Rule, patchConfig.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
func patchConfig() {
|
||||
|
||||
@@ -7,13 +7,18 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type InitParams struct {
|
||||
HomeDir string `json:"home-dir"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type ConfigExtendedParams struct {
|
||||
IsPatch bool `json:"is-patch"`
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
|
||||
IsPatch bool `json:"is-patch"`
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
OverrideRule bool `json:"override-rule"`
|
||||
}
|
||||
|
||||
type GenerateConfigParams struct {
|
||||
@@ -71,15 +76,13 @@ const (
|
||||
stopLogMethod Method = "stopLog"
|
||||
startListenerMethod Method = "startListener"
|
||||
stopListenerMethod Method = "stopListener"
|
||||
startTunMethod Method = "startTun"
|
||||
stopTunMethod Method = "stopTun"
|
||||
updateDnsMethod Method = "updateDns"
|
||||
setProcessMapMethod Method = "setProcessMap"
|
||||
setFdMapMethod Method = "setFdMap"
|
||||
setStateMethod Method = "setState"
|
||||
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
|
||||
getRunTimeMethod Method = "getRunTime"
|
||||
getCurrentProfileNameMethod Method = "getCurrentProfileName"
|
||||
getProfileMethod Method = "getProfile"
|
||||
crashMethod Method = "crash"
|
||||
)
|
||||
|
||||
type Method string
|
||||
@@ -108,20 +111,3 @@ func (message *Message) Json() (string, error) {
|
||||
data, err := json.Marshal(message)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
type InvokeMessage struct {
|
||||
Type InvokeType `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type InvokeType string
|
||||
|
||||
const (
|
||||
ProtectInvoke InvokeType = "protect"
|
||||
ProcessInvoke InvokeType = "process"
|
||||
)
|
||||
|
||||
func (message *InvokeMessage) Json() string {
|
||||
data, _ := json.Marshal(message)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
49
core/go.mod
49
core/go.mod
@@ -6,7 +6,8 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
require (
|
||||
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/samber/lo v1.49.1
|
||||
golang.org/x/sync v0.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -19,28 +20,28 @@ require (
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/ebitengine/purego v0.8.1 // indirect
|
||||
github.com/enfein/mieru/v3 v3.10.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/enfein/mieru/v3 v3.13.0 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.1 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
@@ -50,22 +51,24 @@ require (
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/bart v0.19.0 // indirect
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||
github.com/metacubex/chacha v0.1.0 // indirect
|
||||
github.com/metacubex/chacha v0.1.2 // indirect
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect
|
||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
|
||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.5 // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
|
||||
github.com/metacubex/utls v1.6.6 // indirect
|
||||
github.com/metacubex/utls v1.7.0-alpha.1 // indirect
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
@@ -73,18 +76,17 @@ require (
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
github.com/sagernet/fswatch v0.1.1 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||
github.com/sagernet/sing v0.5.1 // indirect
|
||||
github.com/sagernet/sing v0.5.2 // indirect
|
||||
github.com/sagernet/sing-mux v0.2.1 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
|
||||
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
|
||||
@@ -101,13 +103,12 @@ require (
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
|
||||
104
core/go.sum
104
core/go.sum
@@ -24,12 +24,12 @@ github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ=
|
||||
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
|
||||
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
@@ -43,8 +43,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
|
||||
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
|
||||
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
|
||||
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
@@ -74,8 +74,8 @@ github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
|
||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
@@ -84,6 +84,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
||||
@@ -96,40 +97,45 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
||||
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
|
||||
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc=
|
||||
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
|
||||
github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg=
|
||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic=
|
||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30=
|
||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA=
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c h1:OB3WmMA8YPJjE36RjD9X8xlrWGJ4orxbf2R/KAE28b0=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 h1:vy/8ZYYtWUXYnOnw/NF8ThG1W/RqM/h5rkun+OXZMH0=
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63/go.mod h1:eDZ2JpkSkewGmUlCoLSn2MRFn1D0jKPIys/6aogFx7U=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 h1:hcsz5e5lqhBxn3iQQDIF60FLZ8PQT542GTQZ+1wcIGo=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
|
||||
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
|
||||
github.com/metacubex/utls v1.7.0-alpha.1 h1:oMFsPh2oTlALJ7vKXPJuqgy0YeiZ+q/LLw+ZdxZ80l4=
|
||||
github.com/metacubex/utls v1.7.0-alpha.1/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
@@ -149,8 +155,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
@@ -164,18 +170,16 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJ
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
|
||||
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
|
||||
github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
|
||||
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
|
||||
@@ -218,8 +222,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
@@ -228,11 +232,11 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -248,12 +252,12 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
@@ -263,8 +267,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
39
core/hub.go
39
core/hub.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"core/state"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
@@ -26,18 +27,22 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{
|
||||
OnlyStatisticsProxy: false,
|
||||
}
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{}
|
||||
externalProviders = map[string]cp.Provider{}
|
||||
logSubscriber observable.Subscription[log.Event]
|
||||
currentConfig *config.Config
|
||||
)
|
||||
|
||||
func handleInitClash(homeDirStr string) bool {
|
||||
func handleInitClash(paramsString string) bool {
|
||||
var params = InitParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
version = params.Version
|
||||
if !isInit {
|
||||
constant.SetHomeDir(homeDirStr)
|
||||
constant.SetHomeDir(params.HomeDir)
|
||||
isInit = true
|
||||
}
|
||||
return isInit
|
||||
@@ -152,7 +157,7 @@ func handleChangeProxy(data string, fn func(string string)) {
|
||||
}
|
||||
|
||||
func handleGetTraffic() string {
|
||||
up, down := statistic.DefaultManager.Current(configParams.OnlyStatisticsProxy)
|
||||
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -166,7 +171,7 @@ func handleGetTraffic() string {
|
||||
}
|
||||
|
||||
func handleGetTotalTraffic() string {
|
||||
up, down := statistic.DefaultManager.Total(configParams.OnlyStatisticsProxy)
|
||||
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -179,6 +184,15 @@ func handleGetTotalTraffic() string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleGetProfile(profileId string) string {
|
||||
prof := getRawConfigWithId(profileId)
|
||||
data, err := json.Marshal(prof)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleResetTraffic() {
|
||||
statistic.DefaultManager.ResetStatistic()
|
||||
}
|
||||
@@ -220,6 +234,7 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
||||
if params.TestUrl != "" {
|
||||
testUrl = params.TestUrl
|
||||
}
|
||||
delayData.Url = testUrl
|
||||
|
||||
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
|
||||
if err != nil || delay == 0 {
|
||||
@@ -423,6 +438,14 @@ func handleGetMemory(fn func(value string)) {
|
||||
}()
|
||||
}
|
||||
|
||||
func handleSetState(params string) {
|
||||
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
||||
}
|
||||
|
||||
func handleCrash() {
|
||||
panic("handle invoke crash")
|
||||
}
|
||||
|
||||
func init() {
|
||||
adapter.UrlTestHook = func(url string, name string, delay uint16) {
|
||||
delayData := &Delay{
|
||||
|
||||
@@ -49,8 +49,8 @@ func invokeAction(paramsChar *C.char, port C.longlong) {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
go handleAction(action, func(bytes []byte) {
|
||||
bridge.SendToPort(i, string(bytes))
|
||||
go handleAction(action, func(data interface{}) {
|
||||
bridge.SendToPort(i, string(action.getResult(data)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func sendMessage(message Message) {
|
||||
}
|
||||
bridge.SendToPort(messagePort, string(Action{
|
||||
Method: messageMethod,
|
||||
}.wrapMessage(res)))
|
||||
}.getResult(res)))
|
||||
}
|
||||
|
||||
//export startListener
|
||||
|
||||
@@ -4,6 +4,7 @@ package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
bridge "core/dart-bridge"
|
||||
"core/platform"
|
||||
"core/state"
|
||||
@@ -11,128 +12,116 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/process"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/dns"
|
||||
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Fd struct {
|
||||
Id string `json:"id"`
|
||||
Value int64 `json:"value"`
|
||||
type TunHandler struct {
|
||||
listener *sing_tun.Listener
|
||||
callback unsafe.Pointer
|
||||
|
||||
limit *semaphore.Weighted
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
Id string `json:"id"`
|
||||
Metadata *constant.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type ProcessMapItem struct {
|
||||
Id string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type InvokeManager struct {
|
||||
invokeMap sync.Map
|
||||
chanMap map[string]chan struct{}
|
||||
chanLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewInvokeManager() *InvokeManager {
|
||||
return &InvokeManager{
|
||||
chanMap: make(map[string]chan struct{}),
|
||||
func (t *TunHandler) close() {
|
||||
_ = t.limit.Acquire(context.TODO(), 4)
|
||||
defer t.limit.Release(4)
|
||||
removeTunHook()
|
||||
if t.listener != nil {
|
||||
_ = t.listener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InvokeManager) load(id string) string {
|
||||
res, ok := m.invokeMap.Load(id)
|
||||
if ok {
|
||||
return res.(string)
|
||||
if t.callback != nil {
|
||||
releaseObject(t.callback)
|
||||
}
|
||||
return ""
|
||||
t.callback = nil
|
||||
t.listener = nil
|
||||
}
|
||||
|
||||
func (m *InvokeManager) delete(id string) {
|
||||
m.invokeMap.Delete(id)
|
||||
}
|
||||
func (t *TunHandler) handleProtect(fd int) {
|
||||
_ = t.limit.Acquire(context.Background(), 1)
|
||||
defer t.limit.Release(1)
|
||||
|
||||
func (m *InvokeManager) completer(id string, value string) {
|
||||
m.invokeMap.Store(id, value)
|
||||
m.chanLock.Lock()
|
||||
if ch, ok := m.chanMap[id]; ok {
|
||||
close(ch)
|
||||
delete(m.chanMap, id)
|
||||
}
|
||||
m.chanLock.Unlock()
|
||||
}
|
||||
|
||||
func (m *InvokeManager) await(id string) {
|
||||
m.chanLock.Lock()
|
||||
if _, ok := m.chanMap[id]; !ok {
|
||||
m.chanMap[id] = make(chan struct{})
|
||||
}
|
||||
ch := m.chanMap[id]
|
||||
m.chanLock.Unlock()
|
||||
|
||||
timeout := time.After(500 * time.Millisecond)
|
||||
select {
|
||||
case <-ch:
|
||||
return
|
||||
case <-timeout:
|
||||
m.completer(id, "")
|
||||
if t.listener == nil {
|
||||
return
|
||||
}
|
||||
|
||||
protect(t.callback, fd)
|
||||
}
|
||||
|
||||
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
|
||||
_ = t.limit.Acquire(context.Background(), 1)
|
||||
defer t.limit.Release(1)
|
||||
|
||||
if t.listener == nil {
|
||||
return ""
|
||||
}
|
||||
var protocol int
|
||||
uid := -1
|
||||
switch source.Network() {
|
||||
case "udp", "udp4", "udp6":
|
||||
protocol = syscall.IPPROTO_UDP
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
protocol = syscall.IPPROTO_TCP
|
||||
}
|
||||
if version < 29 {
|
||||
uid = platform.QuerySocketUidFromProcFs(source, target)
|
||||
}
|
||||
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid)
|
||||
}
|
||||
|
||||
var (
|
||||
invokePort int64 = -1
|
||||
tunListener *sing_tun.Listener
|
||||
fdInvokeMap = NewInvokeManager()
|
||||
processInvokeMap = NewInvokeManager()
|
||||
tunLock sync.Mutex
|
||||
runTime *time.Time
|
||||
errBlocked = errors.New("blocked")
|
||||
tunLock sync.Mutex
|
||||
runTime *time.Time
|
||||
errBlocked = errors.New("blocked")
|
||||
tunHandler *TunHandler
|
||||
)
|
||||
|
||||
func handleStartTun(fd int) string {
|
||||
handleStopTun()
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
if fd == 0 {
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
} else {
|
||||
initSocketHook()
|
||||
tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN address: %v", tunListener.Address())
|
||||
}
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
}
|
||||
return handleGetRunTime()
|
||||
}
|
||||
|
||||
func handleStopTun() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
removeSocketHook()
|
||||
runTime = nil
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN close")
|
||||
_ = tunListener.Close()
|
||||
if tunHandler != nil {
|
||||
tunHandler.close()
|
||||
}
|
||||
}
|
||||
|
||||
func handleStartTun(fd int, callback unsafe.Pointer) bool {
|
||||
handleStopTun()
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
if fd != 0 {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
tunHandler = &TunHandler{
|
||||
callback: callback,
|
||||
limit: semaphore.NewWeighted(4),
|
||||
}
|
||||
initTunHook()
|
||||
tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN address: %v", tunListener.Address())
|
||||
} else {
|
||||
removeTunHook()
|
||||
return false
|
||||
}
|
||||
tunHandler.listener = tunListener
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func handleGetRunTime() string {
|
||||
if runTime == nil {
|
||||
return ""
|
||||
@@ -140,99 +129,41 @@ func handleGetRunTime() string {
|
||||
return strconv.FormatInt(runTime.UnixMilli(), 10)
|
||||
}
|
||||
|
||||
func handleSetProcessMap(params string) {
|
||||
var processMapItem = &ProcessMapItem{}
|
||||
err := json.Unmarshal([]byte(params), processMapItem)
|
||||
if err == nil {
|
||||
processInvokeMap.completer(processMapItem.Id, processMapItem.Value)
|
||||
}
|
||||
}
|
||||
|
||||
//export attachInvokePort
|
||||
func attachInvokePort(mPort C.longlong) {
|
||||
invokePort = int64(mPort)
|
||||
}
|
||||
|
||||
func sendInvokeMessage(message InvokeMessage) {
|
||||
if invokePort == -1 {
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(invokePort, message.Json())
|
||||
}
|
||||
|
||||
func handleMarkSocket(fd Fd) {
|
||||
sendInvokeMessage(InvokeMessage{
|
||||
Type: ProtectInvoke,
|
||||
Data: fd,
|
||||
})
|
||||
}
|
||||
|
||||
func handleParseProcess(process Process) {
|
||||
sendInvokeMessage(InvokeMessage{
|
||||
Type: ProcessInvoke,
|
||||
Data: process,
|
||||
})
|
||||
}
|
||||
|
||||
func handleSetFdMap(id string) {
|
||||
go func() {
|
||||
fdInvokeMap.completer(id, "")
|
||||
}()
|
||||
}
|
||||
|
||||
func initSocketHook() {
|
||||
func initTunHook() {
|
||||
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
||||
if platform.ShouldBlockConnection() {
|
||||
return errBlocked
|
||||
}
|
||||
return conn.Control(func(fd uintptr) {
|
||||
fdInt := int64(fd)
|
||||
id := utils.NewUUIDV1().String()
|
||||
|
||||
handleMarkSocket(Fd{
|
||||
Id: id,
|
||||
Value: fdInt,
|
||||
})
|
||||
|
||||
fdInvokeMap.await(id)
|
||||
fdInvokeMap.delete(id)
|
||||
tunHandler.handleProtect(int(fd))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func removeSocketHook() {
|
||||
dialer.DefaultSocketHook = nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
|
||||
if metadata == nil {
|
||||
src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
|
||||
if src == nil || dst == nil {
|
||||
return "", process.ErrInvalidNetwork
|
||||
}
|
||||
id := utils.NewUUIDV1().String()
|
||||
handleParseProcess(Process{
|
||||
Id: id,
|
||||
Metadata: metadata,
|
||||
})
|
||||
processInvokeMap.await(id)
|
||||
res := processInvokeMap.load(id)
|
||||
processInvokeMap.delete(id)
|
||||
return res, nil
|
||||
return tunHandler.handleResolveProcess(src, dst), nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeTunHook() {
|
||||
dialer.DefaultSocketHook = nil
|
||||
process.DefaultPackageNameResolver = nil
|
||||
}
|
||||
|
||||
func handleGetAndroidVpnOptions() string {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
options := state.AndroidVpnOptions{
|
||||
Enable: state.CurrentState.Enable,
|
||||
Enable: state.CurrentState.VpnProps.Enable,
|
||||
Port: currentConfig.General.MixedPort,
|
||||
Ipv4Address: state.DefaultIpv4Address,
|
||||
Ipv6Address: state.GetIpv6Address(),
|
||||
AccessControl: state.CurrentState.AccessControl,
|
||||
SystemProxy: state.CurrentState.SystemProxy,
|
||||
AllowBypass: state.CurrentState.AllowBypass,
|
||||
RouteAddress: state.CurrentState.RouteAddress,
|
||||
AccessControl: state.CurrentState.VpnProps.AccessControl,
|
||||
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
|
||||
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
|
||||
RouteAddress: currentConfig.General.Tun.RouteAddress,
|
||||
BypassDomain: state.CurrentState.BypassDomain,
|
||||
DnsServerAddress: state.GetDnsServerAddress(),
|
||||
}
|
||||
@@ -244,10 +175,6 @@ func handleGetAndroidVpnOptions() string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleSetState(params string) {
|
||||
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
||||
}
|
||||
|
||||
func handleUpdateDns(value string) {
|
||||
go func() {
|
||||
log.Infoln("[DNS] updateDns %s", value)
|
||||
@@ -263,59 +190,34 @@ func handleGetCurrentProfileName() string {
|
||||
return state.CurrentState.CurrentProfileName
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
switch action.Method {
|
||||
case startTunMethod:
|
||||
data := action.Data.(string)
|
||||
var fd int
|
||||
_ = json.Unmarshal([]byte(data), &fd)
|
||||
send(action.wrapMessage(handleStartTun(fd)))
|
||||
return true
|
||||
case stopTunMethod:
|
||||
handleStopTun()
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setStateMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetState(data)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case getAndroidVpnOptionsMethod:
|
||||
send(action.wrapMessage(handleGetAndroidVpnOptions()))
|
||||
result(handleGetAndroidVpnOptions())
|
||||
return true
|
||||
case updateDnsMethod:
|
||||
data := action.Data.(string)
|
||||
handleUpdateDns(data)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setFdMapMethod:
|
||||
fdId := action.Data.(string)
|
||||
handleSetFdMap(fdId)
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setProcessMapMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetProcessMap(data)
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return true
|
||||
case getRunTimeMethod:
|
||||
send(action.wrapMessage(handleGetRunTime()))
|
||||
result(handleGetRunTime())
|
||||
return true
|
||||
case getCurrentProfileNameMethod:
|
||||
send(action.wrapMessage(handleGetCurrentProfileName()))
|
||||
result(handleGetCurrentProfileName())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//export quickStart
|
||||
func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
|
||||
func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
dir := C.GoString(dirChar)
|
||||
paramsString := C.GoString(initParamsChar)
|
||||
bytes := []byte(C.GoString(paramsChar))
|
||||
stateParams := C.GoString(stateParamsChar)
|
||||
go func() {
|
||||
res := handleInitClash(dir)
|
||||
res := handleInitClash(paramsString)
|
||||
if res == false {
|
||||
bridge.SendToPort(i, "init error")
|
||||
}
|
||||
@@ -325,9 +227,8 @@ func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, po
|
||||
}
|
||||
|
||||
//export startTUN
|
||||
func startTUN(fd C.int) *C.char {
|
||||
f := int(fd)
|
||||
return C.CString(handleStartTun(f))
|
||||
func startTUN(fd C.int, callback unsafe.Pointer) bool {
|
||||
return handleStartTun(int(fd), callback)
|
||||
}
|
||||
|
||||
//export getRunTime
|
||||
@@ -340,12 +241,6 @@ func stopTun() {
|
||||
handleStopTun()
|
||||
}
|
||||
|
||||
//export setFdMap
|
||||
func setFdMap(fdIdChar *C.char) {
|
||||
fdId := C.GoString(fdIdChar)
|
||||
handleSetFdMap(fdId)
|
||||
}
|
||||
|
||||
//export getCurrentProfileName
|
||||
func getCurrentProfileName() *C.char {
|
||||
return C.CString(handleGetCurrentProfileName())
|
||||
@@ -367,12 +262,3 @@ func updateDns(s *C.char) {
|
||||
dnsList := C.GoString(s)
|
||||
handleUpdateDns(dnsList)
|
||||
}
|
||||
|
||||
//export setProcessMap
|
||||
func setProcessMap(s *C.char) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
paramsString := C.GoString(s)
|
||||
handleSetProcessMap(paramsString)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
package main
|
||||
|
||||
func nextHandle(action *Action) {
|
||||
return action
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
176
core/platform/procfs.go
Normal file
176
core/platform/procfs.go
Normal file
@@ -0,0 +1,176 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var netIndexOfLocal = -1
|
||||
var netIndexOfUid = -1
|
||||
|
||||
var nativeEndian binary.ByteOrder
|
||||
|
||||
func QuerySocketUidFromProcFs(source, _ net.Addr) int {
|
||||
if netIndexOfLocal < 0 || netIndexOfUid < 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
network := source.Network()
|
||||
|
||||
if strings.HasSuffix(network, "4") || strings.HasSuffix(network, "6") {
|
||||
network = network[:len(network)-1]
|
||||
}
|
||||
|
||||
path := "/proc/net/" + network
|
||||
|
||||
var sIP net.IP
|
||||
var sPort int
|
||||
|
||||
switch s := source.(type) {
|
||||
case *net.TCPAddr:
|
||||
sIP = s.IP
|
||||
sPort = s.Port
|
||||
case *net.UDPAddr:
|
||||
sIP = s.IP
|
||||
sPort = s.Port
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
|
||||
sIP = sIP.To16()
|
||||
if sIP == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
uid := doQuery(path+"6", sIP, sPort)
|
||||
if uid == -1 {
|
||||
sIP = sIP.To4()
|
||||
if sIP == nil {
|
||||
return -1
|
||||
}
|
||||
uid = doQuery(path, sIP, sPort)
|
||||
}
|
||||
|
||||
return uid
|
||||
}
|
||||
|
||||
func doQuery(path string, sIP net.IP, sPort int) int {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
defer func(file *os.File) {
|
||||
_ = file.Close()
|
||||
}(file)
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
|
||||
var bytes [2]byte
|
||||
|
||||
binary.BigEndian.PutUint16(bytes[:], uint16(sPort))
|
||||
|
||||
local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:]))
|
||||
|
||||
for {
|
||||
row, _, err := reader.ReadLine()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(row))
|
||||
|
||||
if len(fields) <= netIndexOfLocal || len(fields) <= netIndexOfUid {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.EqualFold(local, fields[netIndexOfLocal]) {
|
||||
uid, err := strconv.Atoi(fields[netIndexOfUid])
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return uid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nativeEndianIP(ip net.IP) []byte {
|
||||
result := make([]byte, len(ip))
|
||||
|
||||
for i := 0; i < len(ip); i += 4 {
|
||||
value := binary.BigEndian.Uint32(ip[i:])
|
||||
|
||||
nativeEndian.PutUint32(result[i:], value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func init() {
|
||||
file, err := os.Open("/proc/net/tcp")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func(file *os.File) {
|
||||
_ = file.Close()
|
||||
}(file)
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
|
||||
header, _, err := reader.ReadLine()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
columns := strings.Fields(string(header))
|
||||
|
||||
var txQueue, rxQueue, tr, tmWhen bool
|
||||
|
||||
for idx, col := range columns {
|
||||
offset := 0
|
||||
|
||||
if txQueue && rxQueue {
|
||||
offset--
|
||||
}
|
||||
|
||||
if tr && tmWhen {
|
||||
offset--
|
||||
}
|
||||
|
||||
switch col {
|
||||
case "tx_queue":
|
||||
txQueue = true
|
||||
case "rx_queue":
|
||||
rxQueue = true
|
||||
case "tr":
|
||||
tr = true
|
||||
case "tm->when":
|
||||
tmWhen = true
|
||||
case "local_address":
|
||||
netIndexOfLocal = idx + offset
|
||||
case "uid":
|
||||
netIndexOfUid = idx + offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
var x uint32 = 0x01020304
|
||||
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
|
||||
nativeEndian = binary.BigEndian
|
||||
} else {
|
||||
nativeEndian = binary.LittleEndian
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func sendMessage(message Message) {
|
||||
}
|
||||
send(Action{
|
||||
Method: messageMethod,
|
||||
}.wrapMessage(res))
|
||||
}.getResult(res))
|
||||
}
|
||||
|
||||
func send(data []byte) {
|
||||
@@ -61,12 +61,12 @@ func startServer(arg string) {
|
||||
return
|
||||
}
|
||||
|
||||
go handleAction(action, func(bytes []byte) {
|
||||
send(bytes)
|
||||
go handleAction(action, func(data interface{}) {
|
||||
send(action.getResult(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//go:build android && cgo
|
||||
|
||||
package state
|
||||
|
||||
import "net/netip"
|
||||
|
||||
var DefaultIpv4Address = "172.19.0.1/30"
|
||||
var DefaultDnsAddress = "172.19.0.2"
|
||||
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
|
||||
@@ -13,13 +13,14 @@ type AndroidVpnOptions struct {
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
BypassDomain []string `json:"bypassDomain"`
|
||||
RouteAddress []string `json:"routeAddress"`
|
||||
RouteAddress []netip.Prefix `json:"routeAddress"`
|
||||
Ipv4Address string `json:"ipv4Address"`
|
||||
Ipv6Address string `json:"ipv6Address"`
|
||||
DnsServerAddress string `json:"dnsServerAddress"`
|
||||
}
|
||||
|
||||
type AccessControl struct {
|
||||
Enable bool `json:"enable"`
|
||||
Mode string `json:"mode"`
|
||||
AcceptList []string `json:"acceptList"`
|
||||
RejectList []string `json:"rejectList"`
|
||||
@@ -31,20 +32,23 @@ type AndroidVpnRawOptions struct {
|
||||
AccessControl *AccessControl `json:"accessControl"`
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
RouteAddress []string `json:"routeAddress"`
|
||||
Ipv6 bool `json:"ipv6"`
|
||||
BypassDomain []string `json:"bypassDomain"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
AndroidVpnRawOptions
|
||||
CurrentProfileName string `json:"currentProfileName"`
|
||||
VpnProps AndroidVpnRawOptions `json:"vpn-props"`
|
||||
CurrentProfileName string `json:"current-profile-name"`
|
||||
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
|
||||
BypassDomain []string `json:"bypass-domain"`
|
||||
}
|
||||
|
||||
var CurrentState = &State{}
|
||||
var CurrentState = &State{
|
||||
OnlyStatisticsProxy: false,
|
||||
CurrentProfileName: "",
|
||||
}
|
||||
|
||||
func GetIpv6Address() string {
|
||||
if CurrentState.Ipv6 {
|
||||
if CurrentState.VpnProps.Ipv6 {
|
||||
return DefaultIpv6Address
|
||||
} else {
|
||||
return ""
|
||||
|
||||
@@ -33,7 +33,7 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
|
||||
}
|
||||
prefix4 = append(prefix4, tempPrefix4)
|
||||
var prefix6 []netip.Prefix
|
||||
if state.CurrentState.Ipv6 {
|
||||
if state.CurrentState.VpnProps.Ipv6 {
|
||||
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
|
||||
if err != nil {
|
||||
log.Errorln("startTUN error:", err)
|
||||
|
||||
@@ -1,63 +1,30 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/l10n/l10n.dart';
|
||||
import 'package:fl_clash/manager/hotkey_manager.dart';
|
||||
import 'package:fl_clash/manager/manager.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
import 'models/models.dart';
|
||||
import 'pages/pages.dart';
|
||||
|
||||
runAppWithPreferences(
|
||||
Widget child, {
|
||||
required AppState appState,
|
||||
required Config config,
|
||||
required AppFlowingState appFlowingState,
|
||||
required ClashConfig clashConfig,
|
||||
}) {
|
||||
runApp(MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ClashConfig>(
|
||||
create: (_) => clashConfig,
|
||||
),
|
||||
ChangeNotifierProvider<Config>(
|
||||
create: (_) => config,
|
||||
),
|
||||
ChangeNotifierProvider<AppFlowingState>(
|
||||
create: (_) => appFlowingState,
|
||||
),
|
||||
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
|
||||
create: (_) => appState,
|
||||
update: (_, config, clashConfig, appState) {
|
||||
appState?.mode = clashConfig.mode;
|
||||
appState?.selectedMap = config.currentSelectedMap;
|
||||
return appState!;
|
||||
},
|
||||
)
|
||||
],
|
||||
child: child,
|
||||
));
|
||||
}
|
||||
|
||||
class Application extends StatefulWidget {
|
||||
class Application extends ConsumerStatefulWidget {
|
||||
const Application({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Application> createState() => ApplicationState();
|
||||
ConsumerState<Application> createState() => ApplicationState();
|
||||
}
|
||||
|
||||
class ApplicationState extends State<Application> {
|
||||
late SystemColorSchemes systemColorSchemes;
|
||||
class ApplicationState extends ConsumerState<Application> {
|
||||
Timer? _autoUpdateGroupTaskTimer;
|
||||
Timer? _autoUpdateProfilesTaskTimer;
|
||||
|
||||
@@ -73,16 +40,8 @@ class ApplicationState extends State<Application> {
|
||||
ColorScheme _getAppColorScheme({
|
||||
required Brightness brightness,
|
||||
int? primaryColor,
|
||||
required SystemColorSchemes systemColorSchemes,
|
||||
}) {
|
||||
if (primaryColor != null) {
|
||||
return ColorScheme.fromSeed(
|
||||
seedColor: Color(primaryColor),
|
||||
brightness: brightness,
|
||||
);
|
||||
} else {
|
||||
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness);
|
||||
}
|
||||
return ref.read(genColorSchemeProvider(brightness));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -90,12 +49,11 @@ class ApplicationState extends State<Application> {
|
||||
super.initState();
|
||||
_autoUpdateGroupTask();
|
||||
_autoUpdateProfilesTask();
|
||||
globalState.appController = AppController(context);
|
||||
globalState.measure = Measure.of(context);
|
||||
globalState.appController = AppController(context, ref);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
final currentContext = globalState.navigatorKey.currentContext;
|
||||
if (currentContext != null) {
|
||||
globalState.appController = AppController(currentContext);
|
||||
globalState.appController = AppController(currentContext, ref);
|
||||
}
|
||||
await globalState.appController.init();
|
||||
globalState.appController.initLink();
|
||||
@@ -119,7 +77,7 @@ class ApplicationState extends State<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
_buildPlatformWrap(Widget child) {
|
||||
_buildPlatformState(Widget child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowManager(
|
||||
child: TrayManager(
|
||||
@@ -138,18 +96,7 @@ class ApplicationState extends State<Application> {
|
||||
);
|
||||
}
|
||||
|
||||
_buildPage(Widget page) {
|
||||
if (system.isDesktop) {
|
||||
return WindowHeaderContainer(
|
||||
child: page,
|
||||
);
|
||||
}
|
||||
return VpnManager(
|
||||
child: page,
|
||||
);
|
||||
}
|
||||
|
||||
_buildWrap(Widget child) {
|
||||
_buildState(Widget child) {
|
||||
return AppStateManager(
|
||||
child: ClashManager(
|
||||
child: ConnectivityManager(
|
||||
@@ -163,85 +110,72 @@ class ApplicationState extends State<Application> {
|
||||
);
|
||||
}
|
||||
|
||||
_updateSystemColorSchemes(
|
||||
ColorScheme? lightDynamic,
|
||||
ColorScheme? darkDynamic,
|
||||
) {
|
||||
systemColorSchemes = SystemColorSchemes(
|
||||
lightColorScheme: lightDynamic,
|
||||
darkColorScheme: darkDynamic,
|
||||
_buildPlatformApp(Widget child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowHeaderContainer(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return VpnManager(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
_buildApp(Widget child) {
|
||||
return MessageManager(
|
||||
child: ThemeManager(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
globalState.appController.updateSystemColorSchemes(systemColorSchemes);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return _buildPlatformWrap(
|
||||
_buildWrap(
|
||||
Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.appSetting.locale,
|
||||
themeMode: config.themeProps.themeMode,
|
||||
primaryColor: config.themeProps.primaryColor,
|
||||
prueBlack: config.themeProps.prueBlack,
|
||||
fontFamily: config.themeProps.fontFamily,
|
||||
),
|
||||
builder: (_, state, child) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||
return MaterialApp(
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return MessageManager(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
final appController = globalState.appController;
|
||||
final maxWidth = container.maxWidth;
|
||||
if (appController.appState.viewWidth != maxWidth) {
|
||||
globalState.appController.updateViewWidth(maxWidth);
|
||||
}
|
||||
return _buildPage(child!);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: other.getLocaleForString(state.locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: state.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
),
|
||||
return _buildPlatformState(
|
||||
_buildState(
|
||||
Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final locale =
|
||||
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||
final themeProps = ref.watch(themeSettingProvider);
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return AppEnvManager(
|
||||
child: _buildPlatformApp(
|
||||
_buildApp(child!),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
).toPrueBlack(state.prueBlack),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: utils.getLocaleForString(locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: themeProps.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
).toPureBlack(themeProps.pureBlack),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
child: const HomePage(),
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:fl_clash/clash/interface.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
@@ -33,7 +34,7 @@ class ClashCore {
|
||||
}
|
||||
|
||||
static Future<void> initGeo() async {
|
||||
final homePath = await appPath.getHomeDirPath();
|
||||
final homePath = await appPath.homeDirPath;
|
||||
final homeDir = Directory(homePath);
|
||||
final isExists = await homeDir.exists();
|
||||
if (!isExists) {
|
||||
@@ -63,13 +64,19 @@ class ClashCore {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> init({
|
||||
required ClashConfig clashConfig,
|
||||
required Config config,
|
||||
}) async {
|
||||
Future<bool> init() async {
|
||||
await initGeo();
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
return await clashInterface.init(homeDirPath);
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
return await clashInterface.init(
|
||||
InitParams(
|
||||
homeDir: homeDirPath,
|
||||
version: globalState.appState.version,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> setState(CoreState state) async {
|
||||
return await clashInterface.setState(state);
|
||||
}
|
||||
|
||||
shutdown() async {
|
||||
@@ -234,6 +241,14 @@ class ClashCore {
|
||||
return int.parse(value);
|
||||
}
|
||||
|
||||
Future<ClashConfigSnippet?> getProfile(String id) async {
|
||||
final res = await clashInterface.getProfile(id);
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res)));
|
||||
}
|
||||
|
||||
resetTraffic() {
|
||||
clashInterface.resetTraffic();
|
||||
}
|
||||
|
||||
@@ -2348,6 +2348,97 @@ class ClashFFI {
|
||||
|
||||
set suboptarg(ffi.Pointer<ffi.Char> value) => _suboptarg.value = value;
|
||||
|
||||
void protect(
|
||||
protect_func fn,
|
||||
ffi.Pointer<ffi.Void> tun_interface,
|
||||
int fd,
|
||||
) {
|
||||
return _protect(
|
||||
fn,
|
||||
tun_interface,
|
||||
fd,
|
||||
);
|
||||
}
|
||||
|
||||
late final _protectPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
protect_func, ffi.Pointer<ffi.Void>, ffi.Int)>>('protect');
|
||||
late final _protect = _protectPtr
|
||||
.asFunction<void Function(protect_func, ffi.Pointer<ffi.Void>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> resolve_process(
|
||||
resolve_process_func fn,
|
||||
ffi.Pointer<ffi.Void> tun_interface,
|
||||
int protocol,
|
||||
ffi.Pointer<ffi.Char> source,
|
||||
ffi.Pointer<ffi.Char> target,
|
||||
int uid,
|
||||
) {
|
||||
return _resolve_process(
|
||||
fn,
|
||||
tun_interface,
|
||||
protocol,
|
||||
source,
|
||||
target,
|
||||
uid,
|
||||
);
|
||||
}
|
||||
|
||||
late final _resolve_processPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<ffi.Char> Function(
|
||||
resolve_process_func,
|
||||
ffi.Pointer<ffi.Void>,
|
||||
ffi.Int,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Int)>>('resolve_process');
|
||||
late final _resolve_process = _resolve_processPtr.asFunction<
|
||||
ffi.Pointer<ffi.Char> Function(
|
||||
resolve_process_func,
|
||||
ffi.Pointer<ffi.Void>,
|
||||
int,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
int)>();
|
||||
|
||||
void release_object(
|
||||
release_object_func fn,
|
||||
ffi.Pointer<ffi.Void> obj,
|
||||
) {
|
||||
return _release_object(
|
||||
fn,
|
||||
obj,
|
||||
);
|
||||
}
|
||||
|
||||
late final _release_objectPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
release_object_func, ffi.Pointer<ffi.Void>)>>('release_object');
|
||||
late final _release_object = _release_objectPtr
|
||||
.asFunction<void Function(release_object_func, ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void registerCallbacks(
|
||||
protect_func markSocketFunc,
|
||||
resolve_process_func resolveProcessFunc,
|
||||
release_object_func releaseObjectFunc,
|
||||
) {
|
||||
return _registerCallbacks(
|
||||
markSocketFunc,
|
||||
resolveProcessFunc,
|
||||
releaseObjectFunc,
|
||||
);
|
||||
}
|
||||
|
||||
late final _registerCallbacksPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(protect_func, resolve_process_func,
|
||||
release_object_func)>>('registerCallbacks');
|
||||
late final _registerCallbacks = _registerCallbacksPtr.asFunction<
|
||||
void Function(protect_func, resolve_process_func, release_object_func)>();
|
||||
|
||||
void initNativeApiBridge(
|
||||
ffi.Pointer<ffi.Void> api,
|
||||
) {
|
||||
@@ -2443,28 +2534,14 @@ class ClashFFI {
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
|
||||
late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
|
||||
|
||||
void attachInvokePort(
|
||||
int mPort,
|
||||
) {
|
||||
return _attachInvokePort(
|
||||
mPort,
|
||||
);
|
||||
}
|
||||
|
||||
late final _attachInvokePortPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'attachInvokePort');
|
||||
late final _attachInvokePort =
|
||||
_attachInvokePortPtr.asFunction<void Function(int)>();
|
||||
|
||||
void quickStart(
|
||||
ffi.Pointer<ffi.Char> dirChar,
|
||||
ffi.Pointer<ffi.Char> initParamsChar,
|
||||
ffi.Pointer<ffi.Char> paramsChar,
|
||||
ffi.Pointer<ffi.Char> stateParamsChar,
|
||||
int port,
|
||||
) {
|
||||
return _quickStart(
|
||||
dirChar,
|
||||
initParamsChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
port,
|
||||
@@ -2479,19 +2556,21 @@ class ClashFFI {
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> startTUN(
|
||||
int startTUN(
|
||||
int fd,
|
||||
ffi.Pointer<ffi.Void> callback,
|
||||
) {
|
||||
return _startTUN(
|
||||
fd,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
late final _startTUNPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'startTUN');
|
||||
late final _startTUNPtr = _lookup<
|
||||
ffi.NativeFunction<GoUint8 Function(ffi.Int, ffi.Pointer<ffi.Void>)>>(
|
||||
'startTUN');
|
||||
late final _startTUN =
|
||||
_startTUNPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||
_startTUNPtr.asFunction<int Function(int, ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getRunTime() {
|
||||
return _getRunTime();
|
||||
@@ -2511,20 +2590,6 @@ class ClashFFI {
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
|
||||
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
|
||||
|
||||
void setFdMap(
|
||||
ffi.Pointer<ffi.Char> fdIdChar,
|
||||
) {
|
||||
return _setFdMap(
|
||||
fdIdChar,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setFdMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setFdMap');
|
||||
late final _setFdMap =
|
||||
_setFdMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getCurrentProfileName() {
|
||||
return _getCurrentProfileName();
|
||||
}
|
||||
@@ -2572,20 +2637,6 @@ class ClashFFI {
|
||||
'updateDns');
|
||||
late final _updateDns =
|
||||
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void setProcessMap(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _setProcessMap(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setProcessMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setProcessMap');
|
||||
late final _setProcessMap =
|
||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
}
|
||||
|
||||
final class __mbstate_t extends ffi.Union {
|
||||
@@ -3738,6 +3789,31 @@ typedef mode_t = __darwin_mode_t;
|
||||
typedef __darwin_mode_t = __uint16_t;
|
||||
typedef __uint16_t = ffi.UnsignedShort;
|
||||
typedef Dart__uint16_t = int;
|
||||
typedef protect_func = ffi.Pointer<ffi.NativeFunction<protect_funcFunction>>;
|
||||
typedef protect_funcFunction = ffi.Void Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface, ffi.Int fd);
|
||||
typedef Dartprotect_funcFunction = void Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface, int fd);
|
||||
typedef resolve_process_func
|
||||
= ffi.Pointer<ffi.NativeFunction<resolve_process_funcFunction>>;
|
||||
typedef resolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface,
|
||||
ffi.Int protocol,
|
||||
ffi.Pointer<ffi.Char> source,
|
||||
ffi.Pointer<ffi.Char> target,
|
||||
ffi.Int uid);
|
||||
typedef Dartresolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface,
|
||||
int protocol,
|
||||
ffi.Pointer<ffi.Char> source,
|
||||
ffi.Pointer<ffi.Char> target,
|
||||
int uid);
|
||||
typedef release_object_func
|
||||
= ffi.Pointer<ffi.NativeFunction<release_object_funcFunction>>;
|
||||
typedef release_object_funcFunction = ffi.Void Function(
|
||||
ffi.Pointer<ffi.Void> obj);
|
||||
typedef Dartrelease_object_funcFunction = void Function(
|
||||
ffi.Pointer<ffi.Void> obj);
|
||||
|
||||
final class GoInterface extends ffi.Struct {
|
||||
external ffi.Pointer<ffi.Void> t;
|
||||
@@ -3758,6 +3834,8 @@ final class GoSlice extends ffi.Struct {
|
||||
typedef GoInt = GoInt64;
|
||||
typedef GoInt64 = ffi.LongLong;
|
||||
typedef DartGoInt64 = int;
|
||||
typedef GoUint8 = ffi.UnsignedChar;
|
||||
typedef DartGoUint8 = int;
|
||||
|
||||
const int __has_safe_buffers = 1;
|
||||
|
||||
@@ -3973,6 +4051,8 @@ const int __MAC_15_0 = 150000;
|
||||
|
||||
const int __MAC_15_1 = 150100;
|
||||
|
||||
const int __MAC_15_2 = 150200;
|
||||
|
||||
const int __IPHONE_2_0 = 20000;
|
||||
|
||||
const int __IPHONE_2_1 = 20100;
|
||||
@@ -4135,6 +4215,8 @@ const int __IPHONE_18_0 = 180000;
|
||||
|
||||
const int __IPHONE_18_1 = 180100;
|
||||
|
||||
const int __IPHONE_18_2 = 180200;
|
||||
|
||||
const int __WATCHOS_1_0 = 10000;
|
||||
|
||||
const int __WATCHOS_2_0 = 20000;
|
||||
@@ -4233,6 +4315,8 @@ const int __WATCHOS_11_0 = 110000;
|
||||
|
||||
const int __WATCHOS_11_1 = 110100;
|
||||
|
||||
const int __WATCHOS_11_2 = 110200;
|
||||
|
||||
const int __TVOS_9_0 = 90000;
|
||||
|
||||
const int __TVOS_9_1 = 90100;
|
||||
@@ -4333,6 +4417,8 @@ const int __TVOS_18_0 = 180000;
|
||||
|
||||
const int __TVOS_18_1 = 180100;
|
||||
|
||||
const int __TVOS_18_2 = 180200;
|
||||
|
||||
const int __BRIDGEOS_2_0 = 20000;
|
||||
|
||||
const int __BRIDGEOS_3_0 = 30000;
|
||||
@@ -4389,6 +4475,8 @@ const int __BRIDGEOS_9_0 = 90000;
|
||||
|
||||
const int __BRIDGEOS_9_1 = 90100;
|
||||
|
||||
const int __BRIDGEOS_9_2 = 90200;
|
||||
|
||||
const int __DRIVERKIT_19_0 = 190000;
|
||||
|
||||
const int __DRIVERKIT_20_0 = 200000;
|
||||
@@ -4419,6 +4507,8 @@ const int __DRIVERKIT_24_0 = 240000;
|
||||
|
||||
const int __DRIVERKIT_24_1 = 240100;
|
||||
|
||||
const int __DRIVERKIT_24_2 = 240200;
|
||||
|
||||
const int __VISIONOS_1_0 = 10000;
|
||||
|
||||
const int __VISIONOS_1_1 = 10100;
|
||||
@@ -4429,6 +4519,8 @@ const int __VISIONOS_2_0 = 20000;
|
||||
|
||||
const int __VISIONOS_2_1 = 20100;
|
||||
|
||||
const int __VISIONOS_2_2 = 20200;
|
||||
|
||||
const int MAC_OS_X_VERSION_10_0 = 1000;
|
||||
|
||||
const int MAC_OS_X_VERSION_10_1 = 1010;
|
||||
@@ -4555,9 +4647,11 @@ const int MAC_OS_VERSION_15_0 = 150000;
|
||||
|
||||
const int MAC_OS_VERSION_15_1 = 150100;
|
||||
|
||||
const int MAC_OS_VERSION_15_2 = 150200;
|
||||
|
||||
const int __MAC_OS_X_VERSION_MIN_REQUIRED = 150000;
|
||||
|
||||
const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150100;
|
||||
const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150200;
|
||||
|
||||
const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1;
|
||||
|
||||
|
||||
@@ -2,15 +2,12 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/clash/message.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/common/future.dart';
|
||||
import 'package:fl_clash/common/other.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
|
||||
mixin ClashInterface {
|
||||
Future<bool> init(String homeDir);
|
||||
Future<bool> init(InitParams params);
|
||||
|
||||
Future<bool> preload();
|
||||
|
||||
@@ -61,11 +58,17 @@ mixin ClashInterface {
|
||||
|
||||
stopLog();
|
||||
|
||||
Future<bool> crash();
|
||||
|
||||
FutureOr<String> getConnections();
|
||||
|
||||
FutureOr<bool> closeConnection(String id);
|
||||
|
||||
FutureOr<bool> closeConnections();
|
||||
|
||||
FutureOr<String> getProfile(String id);
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
}
|
||||
|
||||
mixin AndroidClashInterface {
|
||||
@@ -73,14 +76,10 @@ mixin AndroidClashInterface {
|
||||
|
||||
Future<bool> setProcessMap(ProcessMapItem item);
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
|
||||
Future<bool> stopTun();
|
||||
// Future<bool> stopTun();
|
||||
|
||||
Future<bool> updateDns(String value);
|
||||
|
||||
Future<DateTime?> startTun(int fd);
|
||||
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions();
|
||||
|
||||
Future<String> getCurrentProfileName();
|
||||
@@ -106,6 +105,8 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
case ActionMethod.closeConnections:
|
||||
case ActionMethod.closeConnection:
|
||||
case ActionMethod.stopListener:
|
||||
case ActionMethod.setState:
|
||||
case ActionMethod.crash:
|
||||
completer?.complete(result.data as bool);
|
||||
return;
|
||||
case ActionMethod.changeProxy:
|
||||
@@ -137,7 +138,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
completer?.complete(result.data);
|
||||
}
|
||||
} catch (_) {
|
||||
debugPrint(result.id);
|
||||
commonPrint.log(result.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +154,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
Duration? timeout,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
}) async {
|
||||
final id = "${method.name}#${other.id}";
|
||||
final id = "${method.name}#${utils.id}";
|
||||
|
||||
callbackCompleterMap[id] = Completer<T>();
|
||||
|
||||
@@ -191,10 +192,18 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> init(String homeDir) {
|
||||
Future<bool> init(InitParams params) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.initClash,
|
||||
data: homeDir,
|
||||
data: json.encode(params),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setState(CoreState state) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setState,
|
||||
data: json.encode(state),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -232,6 +241,15 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
return await invoke<String>(
|
||||
method: ActionMethod.updateConfig,
|
||||
data: json.encode(updateConfigParams),
|
||||
timeout: Duration(minutes: 2),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future<bool> crash() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.crash,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -239,6 +257,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
Future<String> getProxies() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getProxies,
|
||||
timeout: Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -268,9 +287,9 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
@override
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.updateGeoData,
|
||||
data: json.encode(params),
|
||||
);
|
||||
method: ActionMethod.updateGeoData,
|
||||
data: json.encode(params),
|
||||
timeout: Duration(minutes: 1));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -292,6 +311,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.updateExternalProvider,
|
||||
data: providerName,
|
||||
timeout: Duration(minutes: 1),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -317,6 +337,14 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getProfile(String id) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getProfile,
|
||||
data: id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTotalTraffic() {
|
||||
return invoke<String>(
|
||||
|
||||
@@ -67,7 +67,6 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
switch (result.method) {
|
||||
case ActionMethod.setFdMap:
|
||||
case ActionMethod.setProcessMap:
|
||||
case ActionMethod.setState:
|
||||
case ActionMethod.stopTun:
|
||||
case ActionMethod.updateDns:
|
||||
completer?.complete(result.data as bool);
|
||||
@@ -123,33 +122,12 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setState(CoreState state) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setState,
|
||||
data: json.encode(state),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime?> startTun(int fd) async {
|
||||
final res = await invoke<String>(
|
||||
method: ActionMethod.startTun,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopTun() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.stopTun,
|
||||
);
|
||||
}
|
||||
// @override
|
||||
// Future<bool> stopTun() {
|
||||
// return invoke<bool>(
|
||||
// method: ActionMethod.stopTun,
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
|
||||
@@ -233,37 +211,12 @@ class ClashLibHandler {
|
||||
);
|
||||
}
|
||||
|
||||
attachInvokePort(int invokePort) {
|
||||
clashFFI.attachInvokePort(
|
||||
invokePort,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? startTun(int fd) {
|
||||
final runTimeRaw = clashFFI.startTUN(fd);
|
||||
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(runTimeRaw);
|
||||
if (runTimeString.isEmpty) return null;
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
|
||||
stopTun() {
|
||||
clashFFI.stopTun();
|
||||
}
|
||||
|
||||
updateDns(String dns) {
|
||||
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateDns(dnsChar);
|
||||
malloc.free(dnsChar);
|
||||
}
|
||||
|
||||
setProcessMap(ProcessMapItem processMapItem) {
|
||||
final processMapItemChar =
|
||||
json.encode(processMapItem).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setProcessMap(processMapItemChar);
|
||||
malloc.free(processMapItemChar);
|
||||
}
|
||||
|
||||
setState(CoreState state) {
|
||||
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setState(stateChar);
|
||||
@@ -314,14 +267,8 @@ class ClashLibHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
setFdMap(String id) {
|
||||
final idChar = id.toNativeUtf8().cast<Char>();
|
||||
clashFFI.setFdMap(idChar);
|
||||
malloc.free(idChar);
|
||||
}
|
||||
|
||||
Future<String> quickStart(
|
||||
String homeDir,
|
||||
InitParams initParams,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
CoreState state,
|
||||
) {
|
||||
@@ -334,17 +281,18 @@ class ClashLibHandler {
|
||||
}
|
||||
});
|
||||
final params = json.encode(updateConfigParams);
|
||||
final initValue = json.encode(initParams);
|
||||
final stateParams = json.encode(state);
|
||||
final homeChar = homeDir.toNativeUtf8().cast<Char>();
|
||||
final initParamsChar = initValue.toNativeUtf8().cast<Char>();
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
|
||||
clashFFI.quickStart(
|
||||
homeChar,
|
||||
initParamsChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(homeChar);
|
||||
malloc.free(initParamsChar);
|
||||
malloc.free(paramsChar);
|
||||
malloc.free(stateParamsChar);
|
||||
return completer.future;
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'dart:typed_data';
|
||||
import 'package:fl_clash/clash/interface.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/core.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
class ClashService extends ClashHandlerInterface {
|
||||
static ClashService? _instance;
|
||||
@@ -14,6 +15,8 @@ class ClashService extends ClashHandlerInterface {
|
||||
|
||||
Completer<Socket> socketCompleter = Completer();
|
||||
|
||||
bool isStarting = false;
|
||||
|
||||
Process? process;
|
||||
|
||||
factory ClashService() {
|
||||
@@ -27,48 +30,61 @@ class ClashService extends ClashHandlerInterface {
|
||||
}
|
||||
|
||||
_initServer() async {
|
||||
final address = !Platform.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
type: InternetAddressType.unix,
|
||||
)
|
||||
: InternetAddress(
|
||||
localhost,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
await _deleteSocketFile();
|
||||
final server = await ServerSocket.bind(
|
||||
address,
|
||||
0,
|
||||
shared: true,
|
||||
);
|
||||
serverCompleter.complete(server);
|
||||
await for (final socket in server) {
|
||||
await _destroySocket();
|
||||
socketCompleter.complete(socket);
|
||||
socket
|
||||
.transform(
|
||||
StreamTransformer<Uint8List, String>.fromHandlers(
|
||||
handleData: (Uint8List data, EventSink<String> sink) {
|
||||
sink.add(utf8.decode(data, allowMalformed: true));
|
||||
runZonedGuarded(() async {
|
||||
final address = !Platform.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
type: InternetAddressType.unix,
|
||||
)
|
||||
: InternetAddress(
|
||||
localhost,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
await _deleteSocketFile();
|
||||
final server = await ServerSocket.bind(
|
||||
address,
|
||||
0,
|
||||
shared: true,
|
||||
);
|
||||
serverCompleter.complete(server);
|
||||
await for (final socket in server) {
|
||||
await _destroySocket();
|
||||
socketCompleter.complete(socket);
|
||||
socket
|
||||
.transform(
|
||||
StreamTransformer<Uint8List, String>.fromHandlers(
|
||||
handleData: (Uint8List data, EventSink<String> sink) {
|
||||
sink.add(utf8.decode(data, allowMalformed: true));
|
||||
},
|
||||
),
|
||||
)
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}, (error, stack) {
|
||||
commonPrint.log(error.toString());
|
||||
if(error is SocketException){
|
||||
globalState.showNotifier(error.toString());
|
||||
globalState.appController.restartCore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
reStart() async {
|
||||
if (isStarting == true) {
|
||||
return;
|
||||
}
|
||||
isStarting = true;
|
||||
socketCompleter = Completer();
|
||||
if (process != null) {
|
||||
await shutdown();
|
||||
}
|
||||
@@ -90,6 +106,7 @@ class ClashService extends ClashHandlerInterface {
|
||||
],
|
||||
);
|
||||
process!.stdout.listen((_) {});
|
||||
isStarting = false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -125,7 +142,6 @@ class ClashService extends ClashHandlerInterface {
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
await super.shutdown();
|
||||
if (Platform.isWindows) {
|
||||
await request.stopCoreByHelper();
|
||||
}
|
||||
|
||||
@@ -1,28 +1,92 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension ColorExtension on Color {
|
||||
|
||||
Color get toLight {
|
||||
return withOpacity(0.8);
|
||||
Color get opacity80 {
|
||||
return withAlpha(204);
|
||||
}
|
||||
|
||||
Color get toLighter {
|
||||
return withOpacity(0.6);
|
||||
Color get opacity60 {
|
||||
return withAlpha(153);
|
||||
}
|
||||
|
||||
Color get toSoft {
|
||||
return withOpacity(0.12);
|
||||
Color get opacity50 {
|
||||
return withAlpha(128);
|
||||
}
|
||||
|
||||
Color get toLittle {
|
||||
return withOpacity(0.03);
|
||||
Color get opacity38 {
|
||||
return withAlpha(97);
|
||||
}
|
||||
|
||||
Color darken([double amount = .1]) {
|
||||
assert(amount >= 0 && amount <= 1);
|
||||
final hsl = HSLColor.fromColor(this);
|
||||
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||
return hslDark.toColor();
|
||||
Color get opacity30 {
|
||||
return withAlpha(77);
|
||||
}
|
||||
|
||||
Color get opacity15 {
|
||||
return withAlpha(38);
|
||||
}
|
||||
|
||||
Color get opacity10 {
|
||||
return withAlpha(15);
|
||||
}
|
||||
|
||||
Color get opacity3 {
|
||||
return withAlpha(76);
|
||||
}
|
||||
|
||||
Color get opacity0 {
|
||||
return withAlpha(0);
|
||||
}
|
||||
|
||||
int get value32bit {
|
||||
return _floatToInt8(a) << 24 |
|
||||
_floatToInt8(r) << 16 |
|
||||
_floatToInt8(g) << 8 |
|
||||
_floatToInt8(b) << 0;
|
||||
}
|
||||
|
||||
int get alpha8bit => (0xff000000 & value32bit) >> 24;
|
||||
|
||||
int get red8bit => (0x00ff0000 & value32bit) >> 16;
|
||||
|
||||
int get green8bit => (0x0000ff00 & value32bit) >> 8;
|
||||
|
||||
int get blue8bit => (0x000000ff & value32bit) >> 0;
|
||||
|
||||
int _floatToInt8(double x) {
|
||||
return (x * 255.0).round() & 0xff;
|
||||
}
|
||||
|
||||
Color lighten([double amount = 10]) {
|
||||
if (amount <= 0) return this;
|
||||
if (amount > 100) return Colors.white;
|
||||
final HSLColor hsl = this == const Color(0xFF000000)
|
||||
? HSLColor.fromColor(this).withSaturation(0)
|
||||
: HSLColor.fromColor(this);
|
||||
return hsl
|
||||
.withLightness(min(1, max(0, hsl.lightness + amount / 100)))
|
||||
.toColor();
|
||||
}
|
||||
|
||||
String get hex {
|
||||
final value = toARGB32();
|
||||
final red = (value >> 16) & 0xFF;
|
||||
final green = (value >> 8) & 0xFF;
|
||||
final blue = value & 0xFF;
|
||||
return '#${red.toRadixString(16).padLeft(2, '0')}'
|
||||
'${green.toRadixString(16).padLeft(2, '0')}'
|
||||
'${blue.toRadixString(16).padLeft(2, '0')}'
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
Color darken([final int amount = 10]) {
|
||||
if (amount <= 0) return this;
|
||||
if (amount > 100) return Colors.black;
|
||||
final HSLColor hsl = HSLColor.fromColor(this);
|
||||
return hsl
|
||||
.withLightness(min(1, max(0, hsl.lightness - amount / 100)))
|
||||
.toColor();
|
||||
}
|
||||
|
||||
Color blendDarken(
|
||||
@@ -51,11 +115,11 @@ extension ColorExtension on Color {
|
||||
}
|
||||
|
||||
extension ColorSchemeExtension on ColorScheme {
|
||||
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
|
||||
ColorScheme toPureBlack(bool isPrueBlack) => isPrueBlack
|
||||
? copyWith(
|
||||
surface: Colors.black,
|
||||
surfaceContainer: surfaceContainer.darken(
|
||||
0.05,
|
||||
5,
|
||||
),
|
||||
)
|
||||
: this;
|
||||
|
||||
@@ -19,7 +19,7 @@ export 'navigation.dart';
|
||||
export 'navigator.dart';
|
||||
export 'network.dart';
|
||||
export 'num.dart';
|
||||
export 'other.dart';
|
||||
export 'utils.dart';
|
||||
export 'package.dart';
|
||||
export 'path.dart';
|
||||
export 'picker.dart';
|
||||
@@ -34,4 +34,6 @@ export 'text.dart';
|
||||
export 'tray.dart';
|
||||
export 'window.dart';
|
||||
export 'windows.dart';
|
||||
export 'render.dart';
|
||||
export 'render.dart';
|
||||
export 'mixin.dart';
|
||||
export 'print.dart';
|
||||
@@ -11,14 +11,18 @@ import 'package:flutter/material.dart';
|
||||
const appName = "FlClash";
|
||||
const appHelperService = "FlClashHelperService";
|
||||
const coreName = "clash.meta";
|
||||
const browserUa =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||
const packageName = "com.follow.clash";
|
||||
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
|
||||
const helperPort = 47890;
|
||||
const helperTag = "2024125";
|
||||
const baseInfoEdgeInsets = EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
final baseInfoEdgeInsets = EdgeInsets.symmetric(
|
||||
vertical: 16.ap,
|
||||
horizontal: 16.ap,
|
||||
);
|
||||
|
||||
final defaultTextScaleFactor = WidgetsBinding.instance.platformDispatcher.textScaleFactor;
|
||||
const httpTimeoutDuration = Duration(milliseconds: 5000);
|
||||
const moreDuration = Duration(milliseconds: 100);
|
||||
const animateDuration = Duration(milliseconds: 100);
|
||||
@@ -33,30 +37,17 @@ final double kHeaderHeight = system.isDesktop
|
||||
? 40
|
||||
: 28
|
||||
: 0;
|
||||
const GeoXMap defaultGeoXMap = {
|
||||
"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",
|
||||
"geoip":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
|
||||
"geosite":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
|
||||
};
|
||||
const profilesDirectoryName = "profiles";
|
||||
const localhost = "127.0.0.1";
|
||||
const clashConfigKey = "clash_config";
|
||||
const configKey = "config";
|
||||
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
|
||||
const double dialogCommonWidth = 300;
|
||||
const repository = "chen08209/FlClash";
|
||||
const defaultExternalController = "127.0.0.1:9090";
|
||||
const maxMobileWidth = 600;
|
||||
const maxLaptopWidth = 840;
|
||||
const geodataLoaderMemconservative = "memconservative";
|
||||
const geodataLoaderStandard = "standard";
|
||||
const defaultTestUrl = "https://www.gstatic.com/generate_204";
|
||||
final filter = ImageFilter.blur(
|
||||
final commonFilter = ImageFilter.blur(
|
||||
sigmaX: 5,
|
||||
sigmaY: 5,
|
||||
tileMode: TileMode.mirror,
|
||||
@@ -83,12 +74,22 @@ const viewModeColumnsMap = {
|
||||
ViewMode.desktop: [4, 3],
|
||||
};
|
||||
|
||||
const defaultPrimaryColor = Colors.brown;
|
||||
const defaultPrimaryColor = 0xFF795548;
|
||||
|
||||
double getWidgetHeight(num lines) {
|
||||
return max(lines * 84 + (lines - 1) * 16, 0);
|
||||
return max(lines * 84 + (lines - 1) * 16, 0).ap;
|
||||
}
|
||||
|
||||
final mainIsolate = "FlClashMainIsolate";
|
||||
|
||||
final serviceIsolate = "FlClashServiceIsolate";
|
||||
|
||||
const defaultPrimaryColors = [
|
||||
defaultPrimaryColor,
|
||||
0xFF03A9F4,
|
||||
0xFFFFFF00,
|
||||
0XFFBBC9CC,
|
||||
0XFFABD397,
|
||||
0XFFD8C0C3,
|
||||
0XFF665390,
|
||||
];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:fl_clash/manager/manager.dart';
|
||||
import 'package:fl_clash/manager/message_manager.dart';
|
||||
import 'package:fl_clash/widgets/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -11,6 +11,36 @@ extension BuildContextExtension on BuildContext {
|
||||
return findAncestorStateOfType<MessageManagerState>()?.message(text);
|
||||
}
|
||||
|
||||
showSnackBar(
|
||||
String message, {
|
||||
SnackBarAction? action,
|
||||
}) {
|
||||
final width = viewWidth;
|
||||
EdgeInsets margin;
|
||||
if (width < 600) {
|
||||
margin = const EdgeInsets.only(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
left: 16,
|
||||
);
|
||||
} else {
|
||||
margin = EdgeInsets.only(
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: width - 316,
|
||||
);
|
||||
}
|
||||
ScaffoldMessenger.of(this).showSnackBar(
|
||||
SnackBar(
|
||||
action: action,
|
||||
content: Text(message),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
margin: margin,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Size get appSize {
|
||||
return MediaQuery.of(this).size;
|
||||
}
|
||||
@@ -22,4 +52,23 @@ extension BuildContextExtension on BuildContext {
|
||||
ColorScheme get colorScheme => Theme.of(this).colorScheme;
|
||||
|
||||
TextTheme get textTheme => Theme.of(this).textTheme;
|
||||
|
||||
T? findLastStateOfType<T extends State>() {
|
||||
T? state;
|
||||
|
||||
visitor(Element element) {
|
||||
if (!element.mounted) {
|
||||
return;
|
||||
}
|
||||
if (element is StatefulElement) {
|
||||
if (element.state is T) {
|
||||
state = element.state as T;
|
||||
}
|
||||
}
|
||||
element.visitChildren(visitor);
|
||||
}
|
||||
|
||||
visitor(this as Element);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
class Debouncer {
|
||||
Map<dynamic, Timer> operators = {};
|
||||
final Map<dynamic, Timer?> _operations = {};
|
||||
|
||||
call(
|
||||
dynamic tag,
|
||||
@@ -9,14 +9,15 @@ class Debouncer {
|
||||
List<dynamic>? args,
|
||||
Duration duration = const Duration(milliseconds: 600),
|
||||
}) {
|
||||
final timer = operators[tag];
|
||||
final timer = _operations[tag];
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
}
|
||||
operators[tag] = Timer(
|
||||
_operations[tag] = Timer(
|
||||
duration,
|
||||
() {
|
||||
operators.remove(tag);
|
||||
_operations[tag]?.cancel();
|
||||
_operations.remove(tag);
|
||||
Function.apply(
|
||||
func,
|
||||
args,
|
||||
@@ -26,8 +27,61 @@ class Debouncer {
|
||||
}
|
||||
|
||||
cancel(dynamic tag) {
|
||||
operators[tag]?.cancel();
|
||||
_operations[tag]?.cancel();
|
||||
_operations[tag] = null;
|
||||
}
|
||||
}
|
||||
|
||||
class Throttler {
|
||||
final Map<dynamic, Timer?> _operations = {};
|
||||
|
||||
call(
|
||||
dynamic tag,
|
||||
Function func, {
|
||||
List<dynamic>? args,
|
||||
Duration duration = const Duration(milliseconds: 600),
|
||||
}) {
|
||||
final timer = _operations[tag];
|
||||
if (timer != null) {
|
||||
return true;
|
||||
}
|
||||
_operations[tag] = Timer(
|
||||
duration,
|
||||
() {
|
||||
_operations[tag]?.cancel();
|
||||
_operations.remove(tag);
|
||||
Function.apply(
|
||||
func,
|
||||
args,
|
||||
);
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
cancel(dynamic tag) {
|
||||
_operations[tag]?.cancel();
|
||||
_operations[tag] = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> retry<T>({
|
||||
required Future<T> Function() task,
|
||||
int maxAttempts = 3,
|
||||
required bool Function(T res) retryIf,
|
||||
Duration delay = Duration.zero,
|
||||
}) async {
|
||||
int attempts = 0;
|
||||
while (attempts < maxAttempts) {
|
||||
final res = await task();
|
||||
if (!retryIf(res) || attempts >= maxAttempts) {
|
||||
return res;
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
throw "unknown error";
|
||||
}
|
||||
|
||||
final debouncer = Debouncer();
|
||||
|
||||
final throttler = Throttler();
|
||||
|
||||
@@ -10,7 +10,7 @@ extension CompleterExt<T> on Completer<T> {
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
required String functionName,
|
||||
}) {
|
||||
final realTimeout = timeout ?? const Duration(seconds: 1);
|
||||
final realTimeout = timeout ?? const Duration(seconds: 30);
|
||||
Timer(realTimeout + commonDuration, () {
|
||||
if (onLast != null) {
|
||||
onLast();
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../state.dart';
|
||||
import 'constant.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
class FlClashHttpOverrides extends HttpOverrides {
|
||||
static String handleFindProxy(Uri url) {
|
||||
if ([localhost].contains(url.host)) {
|
||||
return "DIRECT";
|
||||
}
|
||||
final port = globalState.config.patchClashConfig.mixedPort;
|
||||
final isStart = globalState.appState.runTime != null;
|
||||
commonPrint.log("find $url proxy:$isStart");
|
||||
if (!isStart) return "DIRECT";
|
||||
return "PROXY localhost:$port";
|
||||
}
|
||||
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
final client = super.createHttpClient(context);
|
||||
client.badCertificateCallback = (_, __, ___) => true;
|
||||
client.findProxy = (url) {
|
||||
if ([localhost].contains(url.host)) {
|
||||
return "DIRECT";
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
final port = appController.clashConfig.mixedPort;
|
||||
final isStart = appController.appFlowingState.isStart;
|
||||
debugPrint("find $url proxy:$isStart");
|
||||
if (!isStart) return "DIRECT";
|
||||
return "PROXY localhost:$port";
|
||||
};
|
||||
client.findProxy = handleFindProxy;
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,3 +65,12 @@ extension DoubleListExt on List<double> {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
extension MapExt<K, V> on Map<K, V> {
|
||||
getCacheValue(K key, V defaultValue) {
|
||||
if (this[key] == null) {
|
||||
this[key] = defaultValue;
|
||||
}
|
||||
return this[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
|
||||
import 'constant.dart';
|
||||
@@ -34,6 +35,9 @@ class AutoLaunch {
|
||||
}
|
||||
|
||||
updateStatus(bool isAutoLaunch) async {
|
||||
if(kDebugMode){
|
||||
return;
|
||||
}
|
||||
if (await isEnable == isAutoLaunch) return;
|
||||
if (isAutoLaunch == true) {
|
||||
enable();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'print.dart';
|
||||
|
||||
typedef InstallConfigCallBack = void Function(String url);
|
||||
|
||||
@@ -15,11 +16,11 @@ class LinkManager {
|
||||
}
|
||||
|
||||
initAppLinksListen(installConfigCallBack) async {
|
||||
debugPrint("initAppLinksListen");
|
||||
commonPrint.log("initAppLinksListen");
|
||||
destroy();
|
||||
subscription = _appLinks.uriLinkStream.listen(
|
||||
(uri) {
|
||||
debugPrint('onAppLink: $uri');
|
||||
commonPrint.log('onAppLink: $uri');
|
||||
if (uri.host == 'install-config') {
|
||||
final parameters = uri.queryParameters;
|
||||
final url = parameters['url'];
|
||||
|
||||
@@ -1,3 +1,72 @@
|
||||
import 'dart:collection';
|
||||
|
||||
class FixedList<T> {
|
||||
final int maxLength;
|
||||
final List<T> _list;
|
||||
|
||||
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
|
||||
|
||||
add(T item) {
|
||||
if (_list.length == maxLength) {
|
||||
_list.removeAt(0);
|
||||
}
|
||||
_list.add(item);
|
||||
}
|
||||
|
||||
clear() {
|
||||
_list.clear();
|
||||
}
|
||||
|
||||
List<T> get list => List.unmodifiable(_list);
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
T operator [](int index) => _list[index];
|
||||
|
||||
FixedList<T> copyWith() {
|
||||
return FixedList(
|
||||
maxLength,
|
||||
list: _list,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FixedMap<K, V> {
|
||||
int maxSize;
|
||||
final Map<K, V> _map = {};
|
||||
final Queue<K> _queue = Queue<K>();
|
||||
|
||||
FixedMap(this.maxSize);
|
||||
|
||||
put(K key, V value) {
|
||||
if (_map.length == maxSize) {
|
||||
final oldestKey = _queue.removeFirst();
|
||||
_map.remove(oldestKey);
|
||||
}
|
||||
_map[key] = value;
|
||||
_queue.add(key);
|
||||
return value;
|
||||
}
|
||||
|
||||
clear() {
|
||||
_map.clear();
|
||||
_queue.clear();
|
||||
}
|
||||
|
||||
updateMaxSize(int size){
|
||||
maxSize = size;
|
||||
}
|
||||
|
||||
V? get(K key) => _map[key];
|
||||
|
||||
|
||||
bool containsKey(K key) => _map.containsKey(key);
|
||||
|
||||
int get length => _map.length;
|
||||
|
||||
Map<K, V> get map => Map.unmodifiable(_map);
|
||||
}
|
||||
|
||||
extension ListExtension<T> on List<T> {
|
||||
List<T> intersection(List<T> list) {
|
||||
return where((item) => list.contains(item)).toList();
|
||||
@@ -17,8 +86,8 @@ extension ListExtension<T> on List<T> {
|
||||
}
|
||||
|
||||
List<T> safeSublist(int start) {
|
||||
if(start <= 0) return this;
|
||||
if(start > length) return [];
|
||||
if (start <= 0) return this;
|
||||
if (start > length) return [];
|
||||
return sublist(start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class SingleInstanceLock {
|
||||
|
||||
Future<bool> acquire() async {
|
||||
try {
|
||||
final lockFilePath = await appPath.getLockFilePath();
|
||||
final lockFilePath = await appPath.lockFilePath;
|
||||
final lockFile = File(lockFilePath);
|
||||
await lockFile.create();
|
||||
_accessFile = await lockFile.open(mode: FileMode.write);
|
||||
|
||||
@@ -3,99 +3,103 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Measure {
|
||||
final TextScaler _textScale;
|
||||
late BuildContext context;
|
||||
final TextScaler _textScaler;
|
||||
final BuildContext context;
|
||||
final Map<String, dynamic> _measureMap;
|
||||
|
||||
Measure.of(this.context)
|
||||
: _textScale = TextScaler.linear(
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
||||
Measure.of(this.context, double textScaleFactor)
|
||||
: _measureMap = {},
|
||||
_textScaler = TextScaler.linear(
|
||||
textScaleFactor,
|
||||
);
|
||||
|
||||
Size computeTextSize(Text text) {
|
||||
Size computeTextSize(
|
||||
Text text, {
|
||||
double maxWidth = double.infinity,
|
||||
}) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(text: text.data, style: text.style),
|
||||
text: TextSpan(
|
||||
text: text.data,
|
||||
style: text.style,
|
||||
),
|
||||
maxLines: text.maxLines,
|
||||
textScaler: _textScale,
|
||||
textScaler: _textScaler,
|
||||
textDirection: text.textDirection ?? TextDirection.ltr,
|
||||
)..layout();
|
||||
)..layout(
|
||||
maxWidth: maxWidth,
|
||||
);
|
||||
return textPainter.size;
|
||||
}
|
||||
|
||||
double? _bodyMediumHeight;
|
||||
Size? _bodyLargeSize;
|
||||
double? _bodySmallHeight;
|
||||
double? _labelSmallHeight;
|
||||
double? _labelMediumHeight;
|
||||
double? _titleLargeHeight;
|
||||
double? _titleMediumHeight;
|
||||
|
||||
double get bodyMediumHeight {
|
||||
_bodyMediumHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
).height;
|
||||
return _bodyMediumHeight!;
|
||||
}
|
||||
|
||||
Size get bodyLargeSize {
|
||||
_bodyLargeSize ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
return _measureMap.getCacheValue(
|
||||
"bodyMediumHeight",
|
||||
computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
return _bodyLargeSize!;
|
||||
}
|
||||
|
||||
double get bodySmallHeight {
|
||||
_bodySmallHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.bodySmall,
|
||||
),
|
||||
).height;
|
||||
return _bodySmallHeight!;
|
||||
return _measureMap.getCacheValue(
|
||||
"bodySmallHeight",
|
||||
computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.bodySmall,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
|
||||
double get labelSmallHeight {
|
||||
_labelSmallHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.labelSmall,
|
||||
),
|
||||
).height;
|
||||
return _labelSmallHeight!;
|
||||
return _measureMap.getCacheValue(
|
||||
"labelSmallHeight",
|
||||
computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.labelSmall,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
|
||||
double get labelMediumHeight {
|
||||
_labelMediumHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.labelMedium,
|
||||
),
|
||||
).height;
|
||||
return _labelMediumHeight!;
|
||||
return _measureMap.getCacheValue(
|
||||
"labelMediumHeight",
|
||||
computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.labelMedium,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
|
||||
double get titleLargeHeight {
|
||||
_titleLargeHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.titleLarge,
|
||||
),
|
||||
).height;
|
||||
return _titleLargeHeight!;
|
||||
return _measureMap.getCacheValue(
|
||||
"titleLargeHeight",
|
||||
computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.titleLarge,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
|
||||
double get titleMediumHeight {
|
||||
_titleMediumHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
).height;
|
||||
return _titleMediumHeight!;
|
||||
return _measureMap.getCacheValue(
|
||||
"titleMediumHeight",
|
||||
computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
53
lib/common/mixin.dart
Normal file
53
lib/common/mixin.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'context.dart';
|
||||
|
||||
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
|
||||
set value(T value) {
|
||||
state = value;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(previous, next) {
|
||||
final res = super.updateShouldNotify(previous, next);
|
||||
if (res) {
|
||||
onUpdate(next);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
onUpdate(T value) {}
|
||||
}
|
||||
|
||||
mixin PageMixin<T extends StatefulWidget> on State<T> {
|
||||
void onPageShow() {
|
||||
initPageState();
|
||||
}
|
||||
|
||||
initPageState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.actions = actions;
|
||||
commonScaffoldState?.floatingActionButton = floatingActionButton;
|
||||
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
|
||||
commonScaffoldState?.updateSearchState(
|
||||
(_) => onSearch != null
|
||||
? AppBarSearchState(
|
||||
onSearch: onSearch!,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void onPageHidden() {}
|
||||
|
||||
List<Widget> get actions => [];
|
||||
|
||||
Widget? get floatingActionButton => null;
|
||||
|
||||
Function(String)? get onSearch => null;
|
||||
|
||||
Function(List<String>)? get onKeywordsUpdate => null;
|
||||
}
|
||||
@@ -6,55 +6,82 @@ import 'package:flutter/material.dart';
|
||||
class Navigation {
|
||||
static Navigation? _instance;
|
||||
|
||||
getItems({
|
||||
List<NavigationItem> getItems({
|
||||
bool openLogs = false,
|
||||
bool hasProxies = false,
|
||||
}) {
|
||||
return [
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.space_dashboard),
|
||||
label: "dashboard",
|
||||
fragment: DashboardFragment(),
|
||||
label: PageLabel.dashboard,
|
||||
keep: false,
|
||||
fragment: DashboardFragment(
|
||||
key: GlobalObjectKey(PageLabel.dashboard),
|
||||
),
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.rocket),
|
||||
label: "proxies",
|
||||
fragment: const ProxiesFragment(),
|
||||
icon: const Icon(Icons.article),
|
||||
label: PageLabel.proxies,
|
||||
fragment: const ProxiesFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.proxies,
|
||||
),
|
||||
),
|
||||
modes: hasProxies
|
||||
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
|
||||
: [],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.folder),
|
||||
label: "profiles",
|
||||
fragment: ProfilesFragment(),
|
||||
label: PageLabel.profiles,
|
||||
fragment: ProfilesFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.profiles,
|
||||
),
|
||||
),
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.view_timeline),
|
||||
label: "requests",
|
||||
fragment: RequestsFragment(),
|
||||
icon: Icon(Icons.view_timeline),
|
||||
label: PageLabel.requests,
|
||||
fragment: RequestsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.requests,
|
||||
),
|
||||
),
|
||||
description: "requestsDesc",
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.ballot),
|
||||
label: "connections",
|
||||
fragment: ConnectionsFragment(),
|
||||
icon: Icon(Icons.ballot),
|
||||
label: PageLabel.connections,
|
||||
fragment: ConnectionsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.connections,
|
||||
),
|
||||
),
|
||||
description: "connectionsDesc",
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.storage),
|
||||
label: "resources",
|
||||
label: PageLabel.resources,
|
||||
description: "resourcesDesc",
|
||||
keep: false,
|
||||
fragment: Resources(),
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
fragment: Resources(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.resources,
|
||||
),
|
||||
),
|
||||
modes: [NavigationItemMode.more],
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.adb),
|
||||
label: "logs",
|
||||
fragment: const LogsFragment(),
|
||||
label: PageLabel.logs,
|
||||
fragment: const LogsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.logs,
|
||||
),
|
||||
),
|
||||
description: "logsDesc",
|
||||
modes: openLogs
|
||||
? [NavigationItemMode.desktop, NavigationItemMode.more]
|
||||
@@ -62,8 +89,12 @@ class Navigation {
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.construction),
|
||||
label: "tools",
|
||||
fragment: ToolsFragment(),
|
||||
label: PageLabel.tools,
|
||||
fragment: ToolsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.tools,
|
||||
),
|
||||
),
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BaseNavigator {
|
||||
static Future<T?> push<T>(BuildContext context, Widget child) async {
|
||||
if (!globalState.appController.isMobileView) {
|
||||
if (globalState.appState.viewMode != ViewMode.mobile) {
|
||||
return await Navigator.of(context).push<T>(
|
||||
CommonDesktopRoute(
|
||||
builder: (context) => child,
|
||||
@@ -68,7 +70,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
|
||||
Duration get transitionDuration => const Duration(milliseconds: 500);
|
||||
|
||||
@override
|
||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
|
||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 500);
|
||||
}
|
||||
|
||||
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
|
||||
@@ -192,7 +194,7 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
|
||||
_primaryPositionCurve = CurvedAnimation(
|
||||
parent: widget.primaryRouteAnimation,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
reverseCurve: Curves.easeInOut,
|
||||
reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped,
|
||||
);
|
||||
_secondaryPositionCurve = CurvedAnimation(
|
||||
parent: widget.secondaryRouteAnimation,
|
||||
@@ -216,9 +218,8 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
|
||||
begin: const _CommonEdgeShadowDecoration(),
|
||||
end: _CommonEdgeShadowDecoration(
|
||||
<Color>[
|
||||
widget.context.colorScheme.inverseSurface.withOpacity(
|
||||
0.06,
|
||||
),
|
||||
widget.context.colorScheme.inverseSurface
|
||||
.withValues(alpha: 0.02),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
@@ -272,7 +273,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
|
||||
return;
|
||||
}
|
||||
|
||||
final double shadowWidth = 0.05 * configuration.size!.width;
|
||||
final double shadowWidth = 1 * configuration.size!.width;
|
||||
final double shadowHeight = configuration.size!.height;
|
||||
final double bandWidth = shadowWidth / (colors.length - 1);
|
||||
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension NumExt on num {
|
||||
String fixed({digit = 2}) {
|
||||
return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
|
||||
String fixed({decimals = 2}) {
|
||||
String formatted = toStringAsFixed(decimals);
|
||||
if (formatted.contains('.')) {
|
||||
formatted = formatted.replaceAll(RegExp(r'0*$'), '');
|
||||
if (formatted.endsWith('.')) {
|
||||
formatted = formatted.substring(0, formatted.length - 1);
|
||||
}
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
double get ap {
|
||||
return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,35 +48,40 @@ class AppPath {
|
||||
return join(executableDirPath, "$appHelperService$executableExtension");
|
||||
}
|
||||
|
||||
Future<String> getDownloadDirPath() async {
|
||||
Future<String> get downloadDirPath async {
|
||||
final directory = await downloadDir.future;
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<String> getHomeDirPath() async {
|
||||
Future<String> get homeDirPath async {
|
||||
final directory = await dataDir.future;
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<String> getLockFilePath() async {
|
||||
Future<String> get lockFilePath async {
|
||||
final directory = await dataDir.future;
|
||||
return join(directory.path, "FlClash.lock");
|
||||
}
|
||||
|
||||
Future<String> getProfilesPath() async {
|
||||
Future<String> get sharedPreferencesPath async {
|
||||
final directory = await dataDir.future;
|
||||
return join(directory.path, "shared_preferences.json");
|
||||
}
|
||||
|
||||
Future<String> get profilesPath async {
|
||||
final directory = await dataDir.future;
|
||||
return join(directory.path, profilesDirectoryName);
|
||||
}
|
||||
|
||||
Future<String?> getProfilePath(String? id) async {
|
||||
if (id == null) return null;
|
||||
final directory = await getProfilesPath();
|
||||
final directory = await profilesPath;
|
||||
return join(directory, "$id.yaml");
|
||||
}
|
||||
|
||||
Future<String?> getProvidersPath(String? id) async {
|
||||
if (id == null) return null;
|
||||
final directory = await getProfilesPath();
|
||||
final directory = await profilesPath;
|
||||
return join(directory, "providers", id);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class Picker {
|
||||
Future<PlatformFile?> pickerFile() async {
|
||||
final filePickerResult = await FilePicker.platform.pickFiles(
|
||||
withData: true,
|
||||
allowMultiple: false,
|
||||
initialDirectory: await appPath.getDownloadDirPath(),
|
||||
initialDirectory: await appPath.downloadDirPath,
|
||||
);
|
||||
return filePickerResult?.files.first;
|
||||
}
|
||||
@@ -18,7 +19,7 @@ class Picker {
|
||||
Future<String?> saveFile(String fileName, Uint8List bytes) async {
|
||||
final path = await FilePicker.platform.saveFile(
|
||||
fileName: fileName,
|
||||
initialDirectory: await appPath.getDownloadDirPath(),
|
||||
initialDirectory: await appPath.downloadDirPath,
|
||||
bytes: Platform.isAndroid ? bytes : null,
|
||||
);
|
||||
if (!Platform.isAndroid && path != null) {
|
||||
@@ -30,9 +31,14 @@ class Picker {
|
||||
|
||||
Future<String?> pickerConfigQRCode() async {
|
||||
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||
final bytes = await xFile?.readAsBytes();
|
||||
if (bytes == null) return null;
|
||||
final result = await other.parseQRCode(bytes);
|
||||
if (xFile == null) {
|
||||
return null;
|
||||
}
|
||||
final controller = MobileScannerController();
|
||||
final capture = await controller.analyzeImage(xFile.path, formats: [
|
||||
BarcodeFormat.qrCode,
|
||||
]);
|
||||
final result = capture?.barcodes.first.rawValue;
|
||||
if (result == null || !result.isUrl) {
|
||||
throw appLocalizations.pleaseUploadValidQrcode;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/models.dart';
|
||||
import 'constant.dart';
|
||||
|
||||
class Preferences {
|
||||
static Preferences? _instance;
|
||||
Completer<SharedPreferences> sharedPreferencesCompleter = Completer();
|
||||
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
|
||||
|
||||
Future<bool> get isInit async =>
|
||||
await sharedPreferencesCompleter.future != null;
|
||||
|
||||
Preferences._internal() {
|
||||
SharedPreferences.getInstance()
|
||||
.then((value) => sharedPreferencesCompleter.complete(value));
|
||||
.then((value) => sharedPreferencesCompleter.complete(value))
|
||||
.onError((_, __) => sharedPreferencesCompleter.complete(null));
|
||||
}
|
||||
|
||||
factory Preferences() {
|
||||
@@ -23,50 +26,38 @@ class Preferences {
|
||||
|
||||
Future<ClashConfig?> getClashConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final clashConfigString = preferences.getString(clashConfigKey);
|
||||
final clashConfigString = preferences?.getString(clashConfigKey);
|
||||
if (clashConfigString == null) return null;
|
||||
final clashConfigMap = json.decode(clashConfigString);
|
||||
try {
|
||||
return ClashConfig.fromJson(clashConfigMap);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
return preferences.setString(
|
||||
clashConfigKey,
|
||||
json.encode(clashConfig),
|
||||
);
|
||||
return ClashConfig.fromJson(clashConfigMap);
|
||||
}
|
||||
|
||||
Future<Config?> getConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final configString = preferences.getString(configKey);
|
||||
final configString = preferences?.getString(configKey);
|
||||
if (configString == null) return null;
|
||||
final configMap = json.decode(configString);
|
||||
try {
|
||||
return Config.fromJson(configMap);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
return null;
|
||||
}
|
||||
return Config.compatibleFromJson(configMap);
|
||||
}
|
||||
|
||||
Future<bool> saveConfig(Config config) async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
return preferences.setString(
|
||||
configKey,
|
||||
json.encode(config),
|
||||
);
|
||||
return await preferences?.setString(
|
||||
configKey,
|
||||
json.encode(config),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
clearClashConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
preferences?.remove(clashConfigKey);
|
||||
}
|
||||
|
||||
clearPreferences() async {
|
||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
||||
sharedPreferencesIns.clear();
|
||||
sharedPreferencesIns?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
final preferences = Preferences();
|
||||
final preferences = Preferences();
|
||||
|
||||
27
lib/common/print.dart
Normal file
27
lib/common/print.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class CommonPrint {
|
||||
static CommonPrint? _instance;
|
||||
|
||||
CommonPrint._internal();
|
||||
|
||||
factory CommonPrint() {
|
||||
_instance ??= CommonPrint._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
log(String? text) {
|
||||
final payload = "[FlClash] $text";
|
||||
debugPrint(payload);
|
||||
if (globalState.isService) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.addLog(
|
||||
Log.app(payload),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final commonPrint = CommonPrint();
|
||||
@@ -14,15 +14,13 @@ class Protocol {
|
||||
|
||||
void register(String scheme) {
|
||||
String protocolRegKey = 'Software\\Classes\\$scheme';
|
||||
RegistryValue protocolRegValue = const RegistryValue(
|
||||
RegistryValue protocolRegValue = RegistryValue.string(
|
||||
'URL Protocol',
|
||||
RegistryValueType.string,
|
||||
'',
|
||||
);
|
||||
String protocolCmdRegKey = 'shell\\open\\command';
|
||||
RegistryValue protocolCmdRegValue = RegistryValue(
|
||||
RegistryValue protocolCmdRegValue = RegistryValue.string(
|
||||
'',
|
||||
RegistryValueType.string,
|
||||
'"${Platform.resolvedExecutable}" "%1"',
|
||||
);
|
||||
final regKey = Registry.currentUser.createKey(protocolRegKey);
|
||||
@@ -31,4 +29,4 @@ class Protocol {
|
||||
}
|
||||
}
|
||||
|
||||
final protocol = Protocol();
|
||||
final protocol = Protocol();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class Render {
|
||||
@@ -22,26 +22,26 @@ class Render {
|
||||
}
|
||||
|
||||
pause() {
|
||||
debouncer.call(
|
||||
"render_pause",
|
||||
throttler.call(
|
||||
DebounceTag.renderPause,
|
||||
_pause,
|
||||
duration: Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
|
||||
resume() {
|
||||
debouncer.cancel("render_pause");
|
||||
throttler.cancel(DebounceTag.renderPause);
|
||||
_resume();
|
||||
}
|
||||
|
||||
void _pause() {
|
||||
void _pause() async {
|
||||
if (_isPaused) return;
|
||||
_isPaused = true;
|
||||
_beginFrame = _dispatcher.onBeginFrame;
|
||||
_drawFrame = _dispatcher.onDrawFrame;
|
||||
_dispatcher.onBeginFrame = null;
|
||||
_dispatcher.onDrawFrame = null;
|
||||
debugPrint("[App] pause");
|
||||
commonPrint.log("pause");
|
||||
}
|
||||
|
||||
void _resume() {
|
||||
@@ -49,8 +49,9 @@ class Render {
|
||||
_isPaused = false;
|
||||
_dispatcher.onBeginFrame = _beginFrame;
|
||||
_dispatcher.onDrawFrame = _drawFrame;
|
||||
debugPrint("[App] resume");
|
||||
_dispatcher.scheduleFrame();
|
||||
commonPrint.log("resume");
|
||||
}
|
||||
}
|
||||
|
||||
final render = system.isDesktop ? Render() : null;
|
||||
final Render? render = system.isDesktop ? Render() : null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -11,33 +11,35 @@ import 'package:flutter/cupertino.dart';
|
||||
|
||||
class Request {
|
||||
late final Dio _dio;
|
||||
late final Dio _clashDio;
|
||||
String? userAgent;
|
||||
|
||||
Request() {
|
||||
_dio = Dio();
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
return handler.next(options); // 继续请求
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
headers: {
|
||||
"User-Agent": browserUa,
|
||||
},
|
||||
),
|
||||
);
|
||||
_clashDio = Dio();
|
||||
_clashDio.httpClientAdapter = IOHttpClientAdapter(createHttpClient: () {
|
||||
final client = HttpClient();
|
||||
client.findProxy = (Uri uri) {
|
||||
client.userAgent = globalState.ua;
|
||||
return FlClashHttpOverrides.handleFindProxy(uri);
|
||||
};
|
||||
return client;
|
||||
});
|
||||
}
|
||||
|
||||
Future<Response> getFileResponseForUrl(String url) async {
|
||||
final response = await _dio
|
||||
.get(
|
||||
url,
|
||||
options: Options(
|
||||
headers: {
|
||||
"User-Agent": globalState.appController.clashConfig.globalUa
|
||||
},
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
)
|
||||
.timeout(
|
||||
httpTimeoutDuration * 6,
|
||||
);
|
||||
final response = await _clashDio.get(
|
||||
url,
|
||||
options: Options(
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -66,36 +68,43 @@ class Request {
|
||||
final remoteVersion = data['tag_name'];
|
||||
final version = globalState.packageInfo.version;
|
||||
final hasUpdate =
|
||||
other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
|
||||
utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
|
||||
if (!hasUpdate) return null;
|
||||
return data;
|
||||
}
|
||||
|
||||
final List<String> _ipInfoSources = [
|
||||
"https://ipwho.is/?fields=ip&output=csv",
|
||||
"https://ipinfo.io/ip",
|
||||
"https://ifconfig.me/ip/",
|
||||
];
|
||||
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
|
||||
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
|
||||
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
|
||||
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
|
||||
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
|
||||
};
|
||||
|
||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||
for (final source in _ipInfoSources) {
|
||||
for (final source in _ipInfoSources.entries) {
|
||||
try {
|
||||
final response = await _dio
|
||||
.get<String>(
|
||||
source,
|
||||
final response = await Dio()
|
||||
.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
)
|
||||
.timeout(httpTimeoutDuration);
|
||||
.timeout(
|
||||
Duration(
|
||||
seconds: 30,
|
||||
),
|
||||
);
|
||||
if (response.statusCode != 200 || response.data == null) {
|
||||
continue;
|
||||
}
|
||||
final ipInfo = await clashCore.getCountryCode(response.data!);
|
||||
if (ipInfo == null && source != _ipInfoSources.last) {
|
||||
if (response.data == null) {
|
||||
continue;
|
||||
}
|
||||
return ipInfo;
|
||||
return source.value(response.data!);
|
||||
} catch (e) {
|
||||
debugPrint("checkIp error ===> $e");
|
||||
commonPrint.log("checkIp error ===> $e");
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||
throw "cancelled";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/widgets/scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BaseScrollBehavior extends MaterialScrollBehavior {
|
||||
@@ -33,10 +35,101 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
|
||||
Widget child,
|
||||
ScrollableDetails details,
|
||||
) {
|
||||
return Scrollbar(
|
||||
interactive: true,
|
||||
return CommonAutoHiddenScrollBar(
|
||||
controller: details.controller,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NextClampingScrollPhysics extends ClampingScrollPhysics {
|
||||
const NextClampingScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
NextClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return NextClampingScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
Simulation? createBallisticSimulation(
|
||||
ScrollMetrics position, double velocity) {
|
||||
final Tolerance tolerance = toleranceFor(position);
|
||||
if (position.outOfRange) {
|
||||
double? end;
|
||||
if (position.pixels > position.maxScrollExtent) {
|
||||
end = position.maxScrollExtent;
|
||||
}
|
||||
if (position.pixels < position.minScrollExtent) {
|
||||
end = position.minScrollExtent;
|
||||
}
|
||||
assert(end != null);
|
||||
return ScrollSpringSimulation(
|
||||
spring,
|
||||
end!,
|
||||
end,
|
||||
min(0.0, velocity),
|
||||
tolerance: tolerance,
|
||||
);
|
||||
}
|
||||
if (velocity.abs() < tolerance.velocity) {
|
||||
return null;
|
||||
}
|
||||
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) {
|
||||
return null;
|
||||
}
|
||||
if (velocity < 0.0 && position.pixels <= position.minScrollExtent) {
|
||||
return null;
|
||||
}
|
||||
return ClampingScrollSimulation(
|
||||
position: position.pixels,
|
||||
velocity: velocity,
|
||||
tolerance: tolerance,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReverseScrollController extends ScrollController {
|
||||
ReverseScrollController({
|
||||
super.initialScrollOffset,
|
||||
super.keepScrollOffset,
|
||||
super.debugLabel,
|
||||
});
|
||||
|
||||
@override
|
||||
ScrollPosition createScrollPosition(
|
||||
ScrollPhysics physics,
|
||||
ScrollContext context,
|
||||
ScrollPosition? oldPosition,
|
||||
) {
|
||||
return ReverseScrollPosition(
|
||||
physics: physics,
|
||||
context: context,
|
||||
initialPixels: initialScrollOffset,
|
||||
keepScrollOffset: keepScrollOffset,
|
||||
oldPosition: oldPosition,
|
||||
debugLabel: debugLabel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReverseScrollPosition extends ScrollPositionWithSingleContext {
|
||||
ReverseScrollPosition({
|
||||
required super.physics,
|
||||
required super.context,
|
||||
super.initialPixels = 0.0,
|
||||
super.keepScrollOffset,
|
||||
super.oldPosition,
|
||||
super.debugLabel,
|
||||
});
|
||||
|
||||
bool _isInit = false;
|
||||
|
||||
@override
|
||||
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
|
||||
if (!_isInit) {
|
||||
correctPixels(maxScrollExtent);
|
||||
_isInit = true;
|
||||
}
|
||||
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
|
||||
}
|
||||
}
|
||||
|
||||
0
lib/common/state.dart
Normal file
0
lib/common/state.dart
Normal file
@@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'print.dart';
|
||||
|
||||
extension StringExtension on String {
|
||||
bool get isUrl {
|
||||
@@ -43,8 +43,17 @@ extension StringExtension on String {
|
||||
RegExp(this);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
commonPrint.log(e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StringExtensionSafe on String? {
|
||||
String getSafeValue(String defaultValue) {
|
||||
if (this == null || this!.isEmpty) {
|
||||
return defaultValue;
|
||||
}
|
||||
return this!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class System {
|
||||
} else if (Platform.isLinux) {
|
||||
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
|
||||
final output = result.stdout.trim();
|
||||
if (output.startsWith('root:') && output.contains('rwx')) {
|
||||
if (output.startsWith('root:') && output.contains('rws')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'color.dart';
|
||||
|
||||
extension TextStyleExtension on TextStyle {
|
||||
TextStyle get toLight => copyWith(color: color?.toLight);
|
||||
TextStyle get toLight => copyWith(color: color?.opacity80);
|
||||
|
||||
TextStyle get toLighter => copyWith(color: color?.toLighter);
|
||||
TextStyle get toLighter => copyWith(color: color?.opacity60);
|
||||
|
||||
TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500);
|
||||
|
||||
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
|
||||
|
||||
TextStyle get toJetBrainsMono => copyWith(
|
||||
fontFamily: FontFamily.jetBrainsMono.value,
|
||||
);
|
||||
|
||||
TextStyle adjustSize(int size) => copyWith(
|
||||
fontSize: fontSize! + size,
|
||||
);
|
||||
|
||||
43
lib/common/theme.dart
Normal file
43
lib/common/theme.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CommonTheme {
|
||||
final BuildContext context;
|
||||
final Map<String, Color> _colorMap;
|
||||
final double textScaleFactor;
|
||||
|
||||
CommonTheme.of(
|
||||
this.context,
|
||||
this.textScaleFactor,
|
||||
) : _colorMap = {};
|
||||
|
||||
Color get darkenSecondaryContainer {
|
||||
return _colorMap.getCacheValue(
|
||||
"darkenSecondaryContainer",
|
||||
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.1),
|
||||
);
|
||||
}
|
||||
|
||||
Color get darkenSecondaryContainerLighter {
|
||||
return _colorMap.getCacheValue(
|
||||
"darkenSecondaryContainerLighter",
|
||||
context.colorScheme.secondaryContainer
|
||||
.blendDarken(context, factor: 0.1)
|
||||
.opacity60,
|
||||
);
|
||||
}
|
||||
|
||||
Color get darken2SecondaryContainer {
|
||||
return _colorMap.getCacheValue(
|
||||
"darken2SecondaryContainer",
|
||||
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.2),
|
||||
);
|
||||
}
|
||||
|
||||
Color get darken3PrimaryContainer {
|
||||
return _colorMap.getCacheValue(
|
||||
"darken3PrimaryContainer",
|
||||
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/utils.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -10,7 +11,6 @@ import 'package:tray_manager/tray_manager.dart';
|
||||
|
||||
import 'app_localizations.dart';
|
||||
import 'constant.dart';
|
||||
import 'other.dart';
|
||||
import 'window.dart';
|
||||
|
||||
class Tray {
|
||||
@@ -25,7 +25,7 @@ class Tray {
|
||||
await trayManager.destroy();
|
||||
}
|
||||
await trayManager.setIcon(
|
||||
other.getTrayIconPath(
|
||||
utils.getTrayIconPath(
|
||||
brightness: brightness ??
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||
),
|
||||
@@ -39,10 +39,7 @@ class Tray {
|
||||
}
|
||||
|
||||
update({
|
||||
required AppState appState,
|
||||
required AppFlowingState appFlowingState,
|
||||
required Config config,
|
||||
required ClashConfig clashConfig,
|
||||
required TrayState trayState,
|
||||
bool focus = false,
|
||||
}) async {
|
||||
if (Platform.isAndroid) {
|
||||
@@ -50,7 +47,7 @@ class Tray {
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
await _updateSystemTray(
|
||||
brightness: appState.brightness,
|
||||
brightness: trayState.brightness,
|
||||
force: focus,
|
||||
);
|
||||
}
|
||||
@@ -63,9 +60,7 @@ class Tray {
|
||||
);
|
||||
menuItems.add(showMenuItem);
|
||||
final startMenuItem = MenuItem.checkbox(
|
||||
label: appFlowingState.isStart
|
||||
? appLocalizations.stop
|
||||
: appLocalizations.start,
|
||||
label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
|
||||
onClick: (_) async {
|
||||
globalState.appController.updateStart();
|
||||
},
|
||||
@@ -80,23 +75,22 @@ class Tray {
|
||||
onClick: (_) {
|
||||
globalState.appController.changeMode(mode);
|
||||
},
|
||||
checked: mode == clashConfig.mode,
|
||||
checked: mode == trayState.mode,
|
||||
),
|
||||
);
|
||||
}
|
||||
menuItems.add(MenuItem.separator());
|
||||
if (!Platform.isWindows) {
|
||||
final groups = appState.currentGroups;
|
||||
for (final group in groups) {
|
||||
if (Platform.isMacOS) {
|
||||
for (final group in trayState.groups) {
|
||||
List<MenuItem> subMenuItems = [];
|
||||
for (final proxy in group.all) {
|
||||
subMenuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: proxy.name,
|
||||
checked: appState.selectedMap[group.name] == proxy.name,
|
||||
checked: trayState.selectedMap[group.name] == proxy.name,
|
||||
onClick: (_) {
|
||||
final appController = globalState.appController;
|
||||
appController.config.updateCurrentSelectedMap(
|
||||
appController.updateCurrentSelectedMap(
|
||||
group.name,
|
||||
proxy.name,
|
||||
);
|
||||
@@ -117,18 +111,18 @@ class Tray {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (groups.isNotEmpty) {
|
||||
if (trayState.groups.isNotEmpty) {
|
||||
menuItems.add(MenuItem.separator());
|
||||
}
|
||||
}
|
||||
if (appFlowingState.isStart) {
|
||||
if (trayState.isStart) {
|
||||
menuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: appLocalizations.tun,
|
||||
onClick: (_) {
|
||||
globalState.appController.updateTun();
|
||||
},
|
||||
checked: clashConfig.tun.enable,
|
||||
checked: trayState.tunEnable,
|
||||
),
|
||||
);
|
||||
menuItems.add(
|
||||
@@ -137,7 +131,7 @@ class Tray {
|
||||
onClick: (_) {
|
||||
globalState.appController.updateSystemProxy();
|
||||
},
|
||||
checked: config.networkProps.systemProxy,
|
||||
checked: trayState.systemProxy,
|
||||
),
|
||||
);
|
||||
menuItems.add(MenuItem.separator());
|
||||
@@ -147,12 +141,12 @@ class Tray {
|
||||
onClick: (_) async {
|
||||
globalState.appController.updateAutoLaunch();
|
||||
},
|
||||
checked: config.appSetting.autoLaunch,
|
||||
checked: trayState.autoLaunch,
|
||||
);
|
||||
final copyEnvVarMenuItem = MenuItem(
|
||||
label: appLocalizations.copyEnvVar,
|
||||
onClick: (_) async {
|
||||
await _copyEnv(clashConfig.mixedPort);
|
||||
await _copyEnv(trayState.port);
|
||||
},
|
||||
);
|
||||
menuItems.add(autoStartMenuItem);
|
||||
@@ -169,12 +163,25 @@ class Tray {
|
||||
await trayManager.setContextMenu(menu);
|
||||
if (Platform.isLinux) {
|
||||
await _updateSystemTray(
|
||||
brightness: appState.brightness,
|
||||
brightness: trayState.brightness,
|
||||
force: focus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateTrayTitle([Traffic? traffic]) async {
|
||||
// if (!Platform.isMacOS) {
|
||||
// return;
|
||||
// }
|
||||
// if (traffic == null) {
|
||||
// await trayManager.setTitle("");
|
||||
// } else {
|
||||
// await trayManager.setTitle(
|
||||
// "${traffic.up.shortShow} ↑ \n${traffic.down.shortShow} ↓",
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> _copyEnv(int port) async {
|
||||
final url = "http://127.0.0.1:$port";
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:lpinyin/lpinyin.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
class Other {
|
||||
class Utils {
|
||||
Color? getDelayColor(int? delay) {
|
||||
if (delay == null) return null;
|
||||
if (delay < 0) return Colors.red;
|
||||
@@ -34,6 +30,26 @@ class Other {
|
||||
);
|
||||
}
|
||||
|
||||
String generateRandomString({int minLength = 10, int maxLength = 100}) {
|
||||
const latinChars =
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
final random = Random();
|
||||
|
||||
int length = minLength + random.nextInt(maxLength - minLength + 1);
|
||||
|
||||
String result = '';
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (random.nextBool()) {
|
||||
result +=
|
||||
String.fromCharCode(0x4E00 + random.nextInt(0x9FA5 - 0x4E00 + 1));
|
||||
} else {
|
||||
result += latinChars[random.nextInt(latinChars.length)];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
String get uuidV4 {
|
||||
final Random random = Random();
|
||||
final bytes = List.generate(16, (_) => random.nextInt(256));
|
||||
@@ -165,30 +181,6 @@ class Other {
|
||||
: "";
|
||||
}
|
||||
|
||||
Future<String?> parseQRCode(Uint8List? bytes) {
|
||||
return Isolate.run<String?>(() {
|
||||
if (bytes == null) return null;
|
||||
img.Image? image = img.decodeImage(bytes);
|
||||
LuminanceSource source = RGBLuminanceSource(
|
||||
image!.width,
|
||||
image.height,
|
||||
image
|
||||
.convert(numChannels: 4)
|
||||
.getBytes(order: img.ChannelOrder.abgr)
|
||||
.buffer
|
||||
.asInt32List(),
|
||||
);
|
||||
final bitmap = BinaryBitmap(GlobalHistogramBinarizer(source));
|
||||
final reader = QRCodeReader();
|
||||
try {
|
||||
final result = reader.decode(bitmap);
|
||||
return result.text;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String? getFileNameForDisposition(String? disposition) {
|
||||
if (disposition == null) return null;
|
||||
final parseValue = HeaderValue.parse(disposition);
|
||||
@@ -241,6 +233,63 @@ class Other {
|
||||
return max((viewWidth / 350).floor(), 1);
|
||||
}
|
||||
|
||||
final _indexPrimary = [
|
||||
50,
|
||||
100,
|
||||
200,
|
||||
300,
|
||||
400,
|
||||
500,
|
||||
600,
|
||||
700,
|
||||
800,
|
||||
850,
|
||||
900,
|
||||
];
|
||||
|
||||
_createPrimarySwatch(Color color) {
|
||||
final Map<int, Color> swatch = <int, Color>{};
|
||||
final int a = color.alpha8bit;
|
||||
final int r = color.red8bit;
|
||||
final int g = color.green8bit;
|
||||
final int b = color.blue8bit;
|
||||
for (final int strength in _indexPrimary) {
|
||||
final double ds = 0.5 - strength / 1000;
|
||||
swatch[strength] = Color.fromARGB(
|
||||
a,
|
||||
r + ((ds < 0 ? r : (255 - r)) * ds).round(),
|
||||
g + ((ds < 0 ? g : (255 - g)) * ds).round(),
|
||||
b + ((ds < 0 ? b : (255 - b)) * ds).round(),
|
||||
);
|
||||
}
|
||||
swatch[50] = swatch[50]!.lighten(18);
|
||||
swatch[100] = swatch[100]!.lighten(16);
|
||||
swatch[200] = swatch[200]!.lighten(14);
|
||||
swatch[300] = swatch[300]!.lighten(10);
|
||||
swatch[400] = swatch[400]!.lighten(6);
|
||||
swatch[700] = swatch[700]!.darken(2);
|
||||
swatch[800] = swatch[800]!.darken(3);
|
||||
swatch[900] = swatch[900]!.darken(4);
|
||||
return MaterialColor(color.value32bit, swatch);
|
||||
}
|
||||
|
||||
List<Color> getMaterialColorShades(Color color) {
|
||||
final swatch = _createPrimarySwatch(color);
|
||||
return <Color>[
|
||||
if (swatch[50] != null) swatch[50]!,
|
||||
if (swatch[100] != null) swatch[100]!,
|
||||
if (swatch[200] != null) swatch[200]!,
|
||||
if (swatch[300] != null) swatch[300]!,
|
||||
if (swatch[400] != null) swatch[400]!,
|
||||
if (swatch[500] != null) swatch[500]!,
|
||||
if (swatch[600] != null) swatch[600]!,
|
||||
if (swatch[700] != null) swatch[700]!,
|
||||
if (swatch[800] != null) swatch[800]!,
|
||||
if (swatch[850] != null) swatch[850]!,
|
||||
if (swatch[900] != null) swatch[900]!,
|
||||
];
|
||||
}
|
||||
|
||||
String getBackupFileName() {
|
||||
return "${appName}_backup_${DateTime.now().show}.zip";
|
||||
}
|
||||
@@ -249,11 +298,6 @@ class Other {
|
||||
return "${appName}_${DateTime.now().show}.log";
|
||||
}
|
||||
|
||||
Size getScreenSize() {
|
||||
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
return view.physicalSize / view.devicePixelRatio;
|
||||
}
|
||||
|
||||
Future<String?> getLocalIpAddress() async {
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
@@ -281,4 +325,4 @@ class Other {
|
||||
}
|
||||
}
|
||||
|
||||
final other = Other();
|
||||
final utils = Utils();
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:screen_retriever/screen_retriever.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class Window {
|
||||
init(WindowProps props, int version) async {
|
||||
init(int version) async {
|
||||
final props = globalState.config.windowProps;
|
||||
final acquire = await singleInstanceLock.acquire();
|
||||
if (!acquire) {
|
||||
exit(0);
|
||||
@@ -20,10 +21,12 @@ class Window {
|
||||
await windowManager.ensureInitialized();
|
||||
WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(props.width, props.height),
|
||||
minimumSize: const Size(380, 500),
|
||||
minimumSize: const Size(380, 400),
|
||||
);
|
||||
if (!Platform.isMacOS || version > 10) {
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
}
|
||||
if (!Platform.isMacOS) {
|
||||
final left = props.left ?? 0;
|
||||
final top = props.top ?? 0;
|
||||
final right = left + props.width;
|
||||
@@ -60,14 +63,16 @@ class Window {
|
||||
}
|
||||
|
||||
show() async {
|
||||
render?.resume();
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
render?.resume();
|
||||
}
|
||||
|
||||
Future<bool> isVisible() async {
|
||||
return await windowManager.isVisible();
|
||||
Future<bool> get isVisible async {
|
||||
final value = await windowManager.isVisible();
|
||||
commonPrint.log("window visible check: $value");
|
||||
return value;
|
||||
}
|
||||
|
||||
close() async {
|
||||
@@ -75,9 +80,9 @@ class Window {
|
||||
}
|
||||
|
||||
hide() async {
|
||||
render?.pause();
|
||||
await windowManager.hide();
|
||||
await windowManager.setSkipTaskbar(true);
|
||||
render?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class Windows {
|
||||
@@ -54,7 +53,7 @@ class Windows {
|
||||
calloc.free(argumentsPtr);
|
||||
calloc.free(operationPtr);
|
||||
|
||||
debugPrint("[Windows] runas: $command $arguments resultCode:$result");
|
||||
commonPrint.log("windows runas: $command $arguments resultCode:$result");
|
||||
|
||||
if (result < 42) {
|
||||
return false;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user