├── .gitattributes
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .metadata
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── co
│ │ │ │ └── lintian
│ │ │ │ └── v2manager
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
├── blog.png
├── github.png
├── magisk.png
└── v2ray.png
├── lib
├── constats.dart
├── main.dart
├── pages
│ ├── config
│ │ ├── proxyconfig.dart
│ │ └── v2config.dart
│ └── home
│ │ └── home.dart
├── route.dart
└── splash.dart
├── pubspec.yaml
└── test
└── widget_test.dart
/.gitattributes:
--------------------------------------------------------------------------------
1 | android/app/src/main/res/mipmap-xhdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
2 | android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
3 | android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
4 | android/app/src/main/res/mipmap-hdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
5 | android/app/src/main/res/mipmap-mdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
6 | assets/github.png filter=lfs diff=lfs merge=lfs -text
7 | assets/magisk.png filter=lfs diff=lfs merge=lfs -text
8 | assets/v2ray.png filter=lfs diff=lfs merge=lfs -text
9 | assets/blog.png filter=lfs diff=lfs merge=lfs -text
10 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # 根据上传的tag,自动编译并发布Release至Github
2 |
3 | name: 发布Github Release
4 |
5 | on:
6 | push:
7 | tags:
8 | - '*'
9 | workflow_dispatch:
10 |
11 | jobs:
12 | publish-release-apk:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: 1.检出代码
16 | uses: actions/checkout@v2
17 |
18 | - name: 2.准备Java环境
19 | uses: actions/setup-java@v2
20 | with:
21 | distribution: 'zulu'
22 | java-version: '11'
23 |
24 | - name: 3.准备Flutter环境
25 | uses: subosito/flutter-action@v2.2.0
26 | with:
27 | flutter-version: '2.8.1'
28 | channel: 'stable'
29 |
30 | - name: 4.解决依赖
31 | run: flutter pub get
32 |
33 | - name: 5.编译apk
34 | run: flutter build apk --target-platform=android-arm,android-arm64 --split-per-abi --obfuscate --split-debug-info=tmp/
35 |
36 | - name: 6.发布至Github-Release
37 | uses: ncipollo/release-action@v1
38 | with:
39 | token: ${{ secrets.GITHUB_TOKEN }}
40 | body: "自动发布"
41 | artifacts: "build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk,build/app/outputs/flutter-apk/app-arm64-v8a-release.apk"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | **/ios/Flutter/.last_build_id
26 | .dart_tool/
27 | .flutter-plugins
28 | .flutter-plugins-dependencies
29 | .packages
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Web related
35 | lib/generated_plugin_registrant.dart
36 |
37 | # Symbolication related
38 | app.*.symbols
39 |
40 | # Obfuscation related
41 | app.*.map.json
42 |
43 | # Android Studio will place build artifacts here
44 | /android/app/debug
45 | /android/app/profile
46 | /android/app/release
47 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # V2ray Manager On Magisk
2 |
3 | 
4 |
5 | ## 关于
6 |
7 | V2M是一个工作于Makgisk下的[V2ray-for-android]插件的管理软件,相较于之前需要在命令行中执行脚本而言,本应用提供了一个更为友好的管理界面.
8 | 也提供了一个比较简易的配置编辑界面。
9 |
10 | ## 运行环境
11 |
12 | - [Magisk](https://github.com/topjohnwu/Magisk) v23.0+
13 | - [V2ray-for-android](https://github.com/yatsuki/v2ray) v2.0.1+
14 |
15 | ## 已经实现的功能
16 | - V2Ray进程的启动/停止
17 | - iptable过滤规则启用/停止/刷新
18 | - V2ray配置文件简易编辑(文本编辑)
19 | - 热点共享子网代理设置
20 | - 分应用代理选择/全局代理启用
21 | 分应用代理对象目前只能选择第三方应用(pm list packages -3)
22 |
23 |
24 | ## 原理实现
25 | 插件原始的管理方式需要在`Shell`环境下执行插件脚本来控制进程的启动/停止等。具体请参照[插件项目页面](https://github.com/yatsuki/v2ray)。
26 | 本应用也是基于插件的命令方式来实现代理的管理,应用启动时会创建一个root`Shell`环境,根据页面的操作,在底层的`Shell`环境执行相应的命令:
27 | ``` dart
28 | _shell = await Process.start("su", [""], mode: ProcessStartMode.detachedWithStdio);
29 | _shell.stdin.writeln('/data/adb/modules/v2ray/system/bin/v2ray --version');
30 | ```
31 |
32 |
33 | ## 预计会增加的功能
34 | - v2ray配置文件的更加直观的编辑页面
35 | - v2ray核心程序更新
36 | - 分应用代理(黑名单模式、白名单模式、系统应用)
37 |
38 | ## 遇到的一些问题
39 |
40 | 在自己的开发环境(`Flutter 2.8.1`、`ArrowOS(Android 12)`、`Redme Note7(lavender)`)出现了下面的问题
41 |
42 | - 虚拟键盘响应缓慢并伴有Exception信息[#2](https://code.lintian.co/ohnoku/v2manager/issues/2)
43 | - 应用后台挂起之后再进入主界面出现黑屏[#1](https://code.lintian.co/ohnoku/v2manager/issues/1)
44 |
45 | 不知道是否是AOSP系统或者是Flutter版本的缘故,总之暂时在自己的环境下还未解决。
46 |
47 |
48 | ## 许可证
49 | 本应用基于[MIT](https://raw.githubusercontent.com/v2fly/v2ray-core/master/LICENSE)发布
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 |
28 | # Additional information about this file can be found at
29 | # https://dart.dev/guides/language/analysis-options
30 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | android {
29 | compileSdkVersion flutter.compileSdkVersion
30 |
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_8
33 | targetCompatibility JavaVersion.VERSION_1_8
34 | }
35 |
36 | kotlinOptions {
37 | jvmTarget = '1.8'
38 | }
39 |
40 | sourceSets {
41 | main.java.srcDirs += 'src/main/kotlin'
42 | }
43 |
44 | aaptOptions {
45 | cruncherEnabled = false
46 | useNewCruncher = false
47 | }
48 |
49 | defaultConfig {
50 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
51 | applicationId "co.lintian.v2manager"
52 | minSdkVersion 21
53 | targetSdkVersion flutter.targetSdkVersion
54 | versionCode flutterVersionCode.toInteger()
55 | versionName flutterVersionName
56 | }
57 |
58 | buildTypes {
59 | release {
60 | // TODO: Add your own signing config for the release build.
61 | // Signing with the debug keys for now, so `flutter run --release` works.
62 | signingConfig signingConfigs.debug
63 | }
64 | }
65 | }
66 |
67 | flutter {
68 | source '../..'
69 | }
70 |
71 | dependencies {
72 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
73 | }
74 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
15 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/co/lintian/v2manager/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package co.lintian.v2manager
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:036ac7600a915539bb5b0e7844924b8d7b636bc24d9616b43a21c7aaf5b50400
3 | size 12146
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:000c378ed3b9253c2f353f887939d39def4bc6926db5743aed132c35e0ccf773
3 | size 10566
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:8799a67af7928a0c10fb81861c22df43fa90a02926a89a4e568f68d2004c8889
3 | size 13012
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:b210e7c2d2532615bb078b177ee1069843f78d6fea1afb37ecf0d3a396be6027
3 | size 15116
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:853f684be12587238851048451cb3efe094c9d02a84a3a8a17e896810eb6f97c
3 | size 14442
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.3.50'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:4.1.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | mavenCentral()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | }
25 | subprojects {
26 | project.evaluationDependsOn(':app')
27 | }
28 |
29 | task clean(type: Delete) {
30 | delete rootProject.buildDir
31 | }
32 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
7 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
4 | def properties = new Properties()
5 |
6 | assert localPropertiesFile.exists()
7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
8 |
9 | def flutterSdkPath = properties.getProperty("flutter.sdk")
10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
12 |
--------------------------------------------------------------------------------
/assets/blog.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:dea5a3be118bdbcbbafd8479fa762dcfd9766485e6490995c53176bedf9f1431
3 | size 13600
4 |
--------------------------------------------------------------------------------
/assets/github.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:ab65a16a3d81f4ea9b9b3a9c1be1d2cf0f422c7233a24d03ad08651ba8026f14
3 | size 13291
4 |
--------------------------------------------------------------------------------
/assets/magisk.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:30c137add270958a5de36664a32a69fbe14ee268fab447087349c957b7120669
3 | size 19372
4 |
--------------------------------------------------------------------------------
/assets/v2ray.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:a1113e6580b230caf08082123fc358d0862c8071be5debb7358504bf2326bc49
3 | size 11903
4 |
--------------------------------------------------------------------------------
/lib/constats.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'dart:io';
3 |
4 | import 'package:async/async.dart' show StreamGroup;
5 |
6 | // 启动页驻留时间
7 | const int splashDuration = 1;
8 |
9 |
10 | // Route 路径定义
11 | const String rHome = '/';
12 | const String rSplash = '/splash';
13 | const String rV2Config = '/v2config';
14 | const String rProxyConfig = '/proxyConfig';
15 |
16 | class Shell {
17 | static Process? _shell;
18 | static Stream? _output;
19 |
20 | Shell() {
21 | initSuperShell();
22 | }
23 |
24 | static Future initSuperShell() async {
25 | if (_shell != null) {
26 | return true;
27 | }
28 | _shell = await Process.start("su", [""], mode: ProcessStartMode.detachedWithStdio);
29 | _output = StreamGroup.merge([_shell!.stdout.transform(utf8.decoder), _shell!.stderr.transform(utf8.decoder)]).asBroadcastStream();
30 | return true;
31 | }
32 |
33 | // 执行命令并舍弃所有输出
34 | static Future runWithNothing(String cmd) async {
35 | // 没有重定向语句的话增加输出重定向
36 | if (!cmd.endsWith(' > /dev/null 2>&1')) {
37 | cmd = cmd + ' > /dev/null 2>&1';
38 | }
39 | await runCmd(cmd);
40 | // _output.drain();
41 | }
42 |
43 | static Future runCmd(String cmd) async {
44 | // _output.drain();
45 | if (_shell== null) {
46 | await initSuperShell();
47 | }
48 | _shell!.stdin.writeln(cmd);
49 | // _output.drain();
50 | }
51 |
52 |
53 | // 执行有输出语句的命令,缓慢输出的场合无法拿到所有输出信息
54 | static Future runWithOutput(String cmd) async {
55 | await runCmd(cmd);
56 | final ret = await _output!.firstWhere((str) => str.isNotEmpty);
57 | _output!.drain();
58 | return ret;
59 | }
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'package:v2manager/constats.dart';
4 | import 'package:v2manager/route.dart';
5 |
6 | void main() {
7 | runApp(const MyApp());
8 | }
9 |
10 | class MyApp extends StatelessWidget {
11 |
12 | const MyApp({Key? key}) : super(key: key);
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 |
17 | // 初始化Root Shell执行环境
18 | Shell();
19 |
20 | // 隐藏底部按钮栏
21 | // SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.top]);
22 | // // 隐藏状态栏
23 | // SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
24 | // // 隐藏状态栏和底部按钮栏
25 | // SystemChrome.setEnabledSystemUIOverlays([]);
26 | return MaterialApp(
27 | debugShowCheckedModeBanner: false,
28 | // title: "V2ray Manager",
29 | theme: ThemeData(
30 | // fontFamily: 'Georgia',
31 | primarySwatch: Colors.green,
32 | buttonTheme: const ButtonThemeData(textTheme: ButtonTextTheme.primary, buttonColor: Colors.green)
33 | ),
34 | routes: routePath,
35 | initialRoute: rHome,
36 | );
37 | }
38 | }
--------------------------------------------------------------------------------
/lib/pages/config/proxyconfig.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'package:v2manager/constats.dart';
4 |
5 | class ProxyConfPage extends StatefulWidget {
6 | const ProxyConfPage({Key? key}) : super(key: key);
7 |
8 | @override
9 | _ProxyConfPageState createState() => _ProxyConfPageState();
10 | }
11 |
12 | class _ProxyConfPageState extends State {
13 |
14 | String _subnet = "";
15 | bool? _isAll = false;
16 | List _apps = [];
17 | final List _appids = [];
18 | final List _appsEnable = [];
19 |
20 | @override
21 | void initState(){
22 | super.initState();
23 | _getApps();
24 | }
25 |
26 | void _getApps() async {
27 | final strs = await Shell.runWithOutput("pm list packages -3");
28 | final enabledApp = await Shell.runWithOutput('cat /data/v2ray/appid.list');
29 | _apps = strs.replaceAll('package:', '').trimRight().split('\n');
30 | for (final namespace in _apps) {
31 | final s = await Shell.runWithOutput("grep '$namespace' /data/system/packages.list");
32 | final i = s.indexOf(' ');
33 | final j = s.indexOf(' ', i+1);
34 | final appid = s.substring(i+1, j);
35 | _appids.add(appid);
36 | _appsEnable.add(enabledApp.contains(appid));
37 | }
38 | setState(() {});
39 | }
40 |
41 | void _toogleAllProxy(bool? val) async {
42 | if (val!) {
43 | await Shell.runCmd("echo '0' > /data/v2ray/appid.list");
44 | for(var i = 0;i< _appsEnable.length;i++){
45 | _appsEnable[i] = false;
46 | }
47 | } else {
48 | await Shell.runCmd("rm /data/v2ray/appid.list");
49 | }
50 | _isAll = val;
51 | setState(() {});
52 | }
53 |
54 | void _toogleAppProxy(bool val, int idx) async {
55 | if (_isAll!) {
56 | return;
57 | }
58 | final appid = _appids[idx];
59 | if (val) {
60 | await Shell.runCmd("echo $appid >> /data/v2ray/appid.list");
61 | } else {
62 | await Shell.runCmd("sed '/$appid/d' /data/v2ray/appid.list > /data/v2ray/appid.tmp && cat /data/v2ray/appid.tmp > /data/v2ray/appid.list && rm /data/v2ray/appid.tmp");
63 | }
64 | setState(() {_appsEnable[idx] = val;});
65 | }
66 |
67 | void _saveShareSubnet() async {
68 | await Shell.runCmd("echo '$_subnet' > /data/v2ray/softap.list");
69 | }
70 |
71 | void _showShareSubnet() {
72 | showDialog(context: context, builder: _buildSubnetModal);
73 | }
74 |
75 | Widget _buildAppListItem(BuildContext context, int idx){
76 | return SwitchListTile(value: _appsEnable[idx], onChanged: (bool val){_toogleAppProxy(val, idx);}, title: Text(_apps[idx]));
77 | }
78 |
79 | Widget _buildSubnetModal(BuildContext context){
80 | return AlertDialog(title: const Text('代理共享热点子网'), content: TextField(onChanged:(String val){_subnet = val;}), actions: [
81 | TextButton(onPressed: (){Navigator.of(context).pop();}, child: const Text('取消')),
82 | TextButton(onPressed: _saveShareSubnet, child: const Text('保存'))
83 | ],);
84 | }
85 |
86 | Widget _buildContent(BuildContext context) {
87 | return SafeArea(child: Column(children: [
88 | CheckboxListTile(value: _isAll, onChanged: _toogleAllProxy, title: const Text("全局代理")),
89 | Expanded(child: ListView.builder(itemCount: _apps.length, shrinkWrap:true, itemBuilder:_buildAppListItem)),
90 | ]));
91 | }
92 |
93 | @override
94 | Widget build(BuildContext context) {
95 | return Scaffold(
96 | appBar: AppBar(
97 | automaticallyImplyLeading: false,
98 | title: const Text("代理对象"),
99 | actions: [
100 | IconButton(onPressed: _showShareSubnet, icon: const Icon(Icons.wifi_tethering_outlined))
101 | ],
102 | ),
103 | body: _buildContent(context),
104 | );
105 | }
106 | }
107 |
108 |
--------------------------------------------------------------------------------
/lib/pages/config/v2config.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/foundation.dart';
4 | import 'package:flutter/material.dart';
5 |
6 | import 'package:v2manager/constats.dart' show Shell;
7 |
8 | class V2ConfPage extends StatefulWidget {
9 | const V2ConfPage({Key? key}) : super(key: key);
10 |
11 | @override
12 | _V2ConfPageState createState() => _V2ConfPageState();
13 | }
14 |
15 | class _V2ConfPageState extends State {
16 |
17 | final PageController _ctl = PageController();
18 | final TextEditingController _strCtl = TextEditingController();
19 | String _cur = "文本编辑";
20 |
21 | String _configStr = "";
22 | Map _configMap = {};
23 |
24 | void _saveToFile() async {
25 | if (kDebugMode) {
26 | // print(_configStr);
27 | // final str = _configStr.replaceAll('\n', '\\n');
28 | // print("echo '$str' > /data/v2ray/config.json");
29 | }
30 | await Shell.runCmd("echo '$_configStr' > /data/v2ray/config.json");
31 | }
32 |
33 | void _readFromFile() async {
34 | _configStr = await Shell.runWithOutput("cat /data/v2ray/config.json");
35 | _configMap = json.decode(_configStr);
36 | if (kDebugMode) {
37 | print(_configMap);
38 | }
39 | _strCtl.text = _configStr;
40 | setState(() { });
41 | }
42 |
43 | void _updateEditingStr(String str) {
44 | _configStr = str;
45 | }
46 |
47 | void _undoChange(){
48 | _readFromFile();
49 | }
50 |
51 | void _updatePageBtn(int page) {
52 | if (page == 0) {
53 | _cur = '文本编辑';
54 | } else {
55 | _cur = '对象编辑';
56 | }
57 | setState(() {});
58 | }
59 |
60 | void _toggleSwitch(){
61 | if (kDebugMode) {
62 | print(_ctl.page);
63 | }
64 | if (_ctl.page==0.0) {
65 | _cur = '对象编辑';
66 | _ctl.jumpToPage(1);
67 | } else {
68 | _cur = '文本编辑';
69 | _ctl.jumpToPage(0);
70 | }
71 | setState(() {});
72 | }
73 |
74 | @override
75 | void initState(){
76 | super.initState();
77 | _readFromFile();
78 | }
79 |
80 | Widget _buildObjectEditor(){
81 | return const Center(child: Text("页面施工中", style: TextStyle(fontSize: 20)));
82 | }
83 |
84 | Widget _buildTextEditor(){
85 | return SingleChildScrollView(child: TextField(keyboardType: TextInputType.text, maxLines: null, controller: _strCtl, onChanged: _updateEditingStr));
86 | }
87 |
88 | Widget _buildContent(BuildContext context) {
89 |
90 | return SafeArea(child:
91 | PageView(onPageChanged: _updatePageBtn, controller: _ctl,children: [_buildObjectEditor(), _buildTextEditor()])
92 | );
93 | }
94 |
95 | @override
96 | Widget build(BuildContext context) {
97 | return Scaffold(
98 | appBar: AppBar(
99 | automaticallyImplyLeading: false,
100 | title: const Text("V2Ray配置"),
101 | actions: [
102 | TextButton(onPressed: _toggleSwitch, child: Text(_cur, style: TextStyle(color:Theme.of(context).cardColor))),
103 | IconButton(onPressed: _saveToFile, icon: const Icon(Icons.save))
104 | ],
105 | ),
106 | floatingActionButton: FloatingActionButton(onPressed: _undoChange, child: const Icon(Icons.refresh_outlined)),
107 | body: _buildContent(context),
108 | );
109 | }
110 | }
111 |
112 |
--------------------------------------------------------------------------------
/lib/pages/home/home.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:v2manager/constats.dart';
3 |
4 | class HomePage extends StatefulWidget {
5 |
6 | const HomePage({Key? key}) : super(key: key);
7 |
8 | @override
9 | _HomePageState createState() => _HomePageState();
10 | }
11 |
12 | class _HomePageState extends State {
13 |
14 | String _platform = "";
15 | int _pid = 0;
16 | String _version = "";
17 | bool _actived = false;
18 |
19 | String _modVersion = "";
20 | final String _modLatest = "20220119";
21 | bool _iptablesActived = false;
22 |
23 | Future _initBasicInfo() async {
24 |
25 | // Check Program exist
26 | final chkRet = await Shell.runWithOutput('md5sum /data/adb/modules/v2ray/system/bin/v2ray');
27 | final modMD5 = chkRet.substring(0, chkRet.indexOf(' '));
28 |
29 | final verRet = await Shell.runWithOutput('/data/adb/modules/v2ray/system/bin/v2ray --version');
30 | _version = verRet.substring(6, verRet.indexOf('('));
31 | _platform = verRet.substring(verRet.lastIndexOf('(') + 1, verRet.lastIndexOf(')'));
32 |
33 |
34 | final enabledRet = await Shell.runWithOutput('md5sum /system/bin/v2ray');
35 | final sysBinMD5 = enabledRet.substring(0, chkRet.indexOf(' '));
36 | _actived = sysBinMD5.compareTo(modMD5) == 0;
37 |
38 |
39 | final pidRet = await Shell.runWithOutput('/data/adb/modules/v2ray/scripts/v2ray.service status');
40 | if (pidRet.indexOf('PID: ') > 0) {
41 | _pid = int.parse(pidRet.substring(pidRet.indexOf('PID: ')+5, pidRet.lastIndexOf(' ')));
42 | }
43 |
44 | // versionCode=20210801
45 | final modVerRet = await Shell.runWithOutput('grep versionCode /data/adb/modules/v2ray/module.prop');
46 | _modVersion = modVerRet.substring(modVerRet.indexOf('=')+1, modVerRet.length).trim();
47 |
48 | // 查看时排除错误输出
49 | final ipRet = await Shell.runWithOutput('iptables -t nat -L V2RAY 2>/dev/null | wc -l');
50 | _iptablesActived = int.parse(ipRet) > 0;
51 |
52 | setState(() {});
53 | }
54 |
55 | Future _toggleV2rayRun() async {
56 | String script = "/data/adb/modules/v2ray/scripts/v2ray.service";
57 | // 执行时丢弃所有输出
58 | await Shell.runWithNothing(script + (_pid > 0? ' stop' : ' start'));
59 |
60 | final pidRet = await Shell.runWithOutput(script + " status");
61 |
62 | setState(() {
63 | if (pidRet.indexOf('PID: ') > 0) {
64 | _pid = int.parse(pidRet.substring(pidRet.indexOf('PID: ') + 5, pidRet.lastIndexOf(' ')));
65 | } else {
66 | _pid = 0;
67 | }
68 | });
69 | }
70 |
71 | // void _toggleV2rayUpdate() {
72 |
73 | // }
74 |
75 | Future _toggleIptablesRules() async {
76 | String script = "/data/adb/modules/v2ray/scripts/v2ray.tproxy";
77 | await Shell.runWithNothing(script + (_iptablesActived ? ' disable' : ' enable'));
78 |
79 | final ipRet = await Shell.runWithOutput("iptables -t nat -L V2RAY 2>/dev/null | wc -l");
80 | var count = int.parse(ipRet);
81 |
82 | setState(() {
83 | _iptablesActived = count > 0;
84 | });
85 |
86 | }
87 |
88 | Future _toggleIptablesFlash() async {
89 | // 丢弃所有输出,不做任何处理
90 | Shell.runWithNothing("/data/adb/modules/v2ray/scripts/v2ray.tproxy renew");
91 |
92 | setState(() { _iptablesActived = true; });
93 | }
94 |
95 |
96 | @override
97 | void initState(){
98 | super.initState();
99 | _initBasicInfo();
100 | }
101 |
102 | Widget _buildV2rayCard() {
103 | return Card(
104 | elevation: 5.0,
105 | color: Theme.of(context).secondaryHeaderColor,
106 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
107 | margin: const EdgeInsets.only(top: 16),
108 | child: Container(margin: const EdgeInsets.only(top:16, left:32, right:32, bottom: 16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
109 | // 第一行 V2ray+按钮
110 | Row(children: [
111 | ImageIcon(const AssetImage("assets/v2ray.png"), size: 32, color: Theme.of(context).primaryColor,),
112 | const SizedBox(width: 8),
113 | Text("主程序", style: TextStyle(fontSize: 28,fontWeight: FontWeight.normal, color: Theme.of(context).primaryColor )),
114 | const Spacer(),
115 | // IconButton(onPressed: () => {}, icon: Icon(Icons.arrow_circle_up_outlined, color: Theme.of(context).primaryColor), tooltip: "更新V2Ray主程序"),
116 | IconButton(onPressed: _toggleV2rayRun, icon: Icon(_pid == 0 ? Icons.play_circle_outlined: Icons.pause_circle_outlined, color: Theme.of(context).primaryColor), tooltip: "启动/停止V2Ray进程"),
117 | ]),
118 | Container(margin: EdgeInsets.zero, child: Column(children:[
119 | // 第二行 版本
120 | Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
121 | const Text("平台:", style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal, color:Colors.grey)),
122 | const SizedBox(width:4),
123 | Text(_platform, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
124 | ]),
125 | Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
126 | const Text("版本:", style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal, color:Colors.grey)),
127 | const SizedBox(width:4),
128 | Text(_version, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
129 | ]),
130 | Row(crossAxisAlignment: CrossAxisAlignment.end,children: [
131 | const Text("启用:" , style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal, color:Colors.grey)),
132 | const SizedBox(width:4),
133 | Text( _actived ? "是" : "否" ,style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
134 | const SizedBox(width:48),
135 | const Text("PID:", style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal, color:Colors.grey)),
136 | const SizedBox(width:4),
137 | Text(_pid.toString(), style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
138 | ]),])),
139 | ])
140 | ));
141 | }
142 |
143 | Widget _buildModuleCard(){
144 |
145 | return Card(
146 | elevation: 5.0,
147 | color: Theme.of(context).secondaryHeaderColor,
148 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
149 | margin: const EdgeInsets.only(top: 16),
150 | child: Container(margin: const EdgeInsets.only(top:16, left:32, right:32, bottom: 16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
151 | // 第一行 标题
152 | Row(children: [
153 | ImageIcon(const AssetImage("assets/magisk.png"), color: Theme.of(context).primaryColor, size: 32,),
154 | const SizedBox(width: 8),
155 | Text("模块", style: TextStyle(fontSize: 28,fontWeight: FontWeight.normal, color: Theme.of(context).primaryColor)),
156 | const Spacer(),
157 | IconButton(onPressed: _toggleIptablesFlash, icon: Icon(Icons.bolt_outlined, color: Theme.of(context).primaryColor), tooltip: "刷新Iptables规则"),
158 | IconButton(onPressed: _toggleIptablesRules, icon: Icon(_iptablesActived ? Icons.stop_circle_outlined : Icons.play_circle_outlined, color: Theme.of(context).primaryColor), tooltip: "启动/停止Iptables拦截"),
159 | ]),
160 | Container(margin: EdgeInsets.zero, child: Column(children:[
161 | // 第二行 版本
162 | Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
163 | const Text("最新:", style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal, color:Colors.grey)),
164 | const SizedBox(width: 4),
165 | Text(_modLatest, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
166 | ]),
167 | Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
168 | const Text("当前:", style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal, color:Colors.grey)),
169 | const SizedBox(width: 4),
170 | Text(_modVersion, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
171 | ]),
172 | Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
173 | const Text("Iptables过滤:", style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal, color: Colors.grey)),
174 | const SizedBox(width: 4),
175 | Text(_iptablesActived ? "过滤中": "未启用", style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
176 | ]),
177 | ])),
178 |
179 | ])
180 | ));
181 | }
182 |
183 | Widget _buildConfigLine(){
184 | return Container(margin: const EdgeInsets.only(top:16, bottom: 16), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
185 | InkWell(onTap:()=>Navigator.pushNamed(context, rV2Config), child:Container(padding: const EdgeInsets.all(4) ,width:150, decoration: BoxDecoration(color: Colors.grey[200], borderRadius: const BorderRadius.all(Radius.circular(8))), child:Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
186 | Icon(Icons.settings, size: 48, color: Theme.of(context).primaryColor),
187 | Text("代理规则", style: TextStyle(color: Theme.of(context).primaryColor)),
188 | ]))),
189 | const SizedBox(width:16),
190 | InkWell(onTap:()=>Navigator.pushNamed(context, rProxyConfig), child:Container(padding: const EdgeInsets.all(4) ,width:150, decoration: BoxDecoration(color: Colors.grey[200], borderRadius: const BorderRadius.all(Radius.circular(8))), child:Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
191 | Icon(Icons.tune, size: 48, color: Theme.of(context).primaryColor),
192 | Text("代理对象", style: TextStyle(color: Theme.of(context).primaryColor)),
193 | ]))),
194 | ]));
195 | }
196 |
197 | Widget _buildSupportCard(){
198 | return Card(
199 | elevation: 5.0,
200 | color: Colors.grey[100],
201 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
202 | margin: EdgeInsets.zero,
203 | child: Container(margin: const EdgeInsets.only(top:16, left:16, right:24, bottom: 16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children:[
204 | const Text("关于", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
205 | const SizedBox(height: 8),
206 | const Text("此应用是基于Magisk的V2ray插件管理应用,使用前请确保已安装Magisk以及V2ray for Android插件且在使用时赋予root权限。", softWrap: true, style: TextStyle(fontSize: 12, color: Colors.grey)),
207 | const SizedBox(height: 16),
208 | const Text("支持", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
209 | const SizedBox(height: 4),
210 | const Text("本应用将一直保持免费开源,感谢大家一如既往的支持", softWrap: true, style: TextStyle(fontSize: 8, color: Colors.grey)),
211 | Row(children: [
212 | IconButton(onPressed: (){}, icon: Image.asset("assets/github.png",), color: Colors.black87, iconSize: 32,),
213 | IconButton(onPressed: (){}, icon: Image.asset("assets/blog.png",), color: Colors.black87, iconSize: 32,),
214 | ]),
215 |
216 | ])));
217 | }
218 |
219 | Widget _buildContent(BuildContext context) {
220 |
221 | return Container(
222 | margin: const EdgeInsets.symmetric(horizontal: 8.0),
223 | padding: const EdgeInsets.symmetric(horizontal: 8.0),
224 | child: Column(children: [
225 | // Main info
226 | _buildV2rayCard(),
227 | _buildModuleCard(),
228 | _buildConfigLine(),
229 | // const Spacer(),
230 | _buildSupportCard()
231 | // Settings
232 | ]
233 | ));
234 |
235 | }
236 |
237 | @override
238 | Widget build(BuildContext context) {
239 |
240 | return Scaffold(
241 | appBar: AppBar(
242 | automaticallyImplyLeading: false,
243 | // title: Text("主页", style: Theme.of(context).textTheme.headline6),
244 |
245 | ),
246 | body: _buildContent(context),
247 |
248 | );
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/lib/route.dart:
--------------------------------------------------------------------------------
1 | import 'package:v2manager/constats.dart';
2 | import 'package:v2manager/pages/config/v2config.dart';
3 | import 'package:v2manager/pages/config/proxyconfig.dart';
4 | import 'package:v2manager/pages/home/home.dart';
5 | import 'package:v2manager/splash.dart';
6 |
7 | var routePath = {
8 | rSplash: (context) => const SplashPage(),
9 | rHome: (context) => const HomePage(),
10 | rV2Config: (context) => const V2ConfPage(),
11 | rProxyConfig: (context) => const ProxyConfPage(),
12 | };
13 |
--------------------------------------------------------------------------------
/lib/splash.dart:
--------------------------------------------------------------------------------
1 | // import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import 'package:v2manager/constats.dart';
5 | import 'package:v2manager/pages/home/home.dart';
6 |
7 | class SplashPage extends StatefulWidget {
8 |
9 | const SplashPage({Key? key}) : super(key: key);
10 |
11 | @override
12 | _SplashPageState createState() => _SplashPageState();
13 | }
14 |
15 | class _SplashPageState extends State {
16 | @override
17 | void initState() {
18 | var d = const Duration(seconds: splashDuration);
19 | Future.delayed(d, () {
20 | Navigator.pushAndRemoveUntil(context,
21 | MaterialPageRoute(builder: (context) {
22 | return const HomePage();
23 | }), (route) => false);
24 | });
25 | super.initState();
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) => const Scaffold(body: Center(child: SizedBox()));
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: v2manager
2 | description: Magisk插件V2ray-For-Android管理应用
3 |
4 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev
5 |
6 | version: 1.0.2-beta
7 |
8 | environment:
9 | sdk: ">=2.15.1 <3.0.0"
10 |
11 | dependencies:
12 | flutter:
13 | sdk: flutter
14 | # url_launcher: ^6.0.18
15 |
16 | dev_dependencies:
17 | flutter_test:
18 | sdk: flutter
19 | flutter_lints: ^1.0.0
20 | flutter:
21 | uses-material-design: true
22 | assets:
23 | - assets/
--------------------------------------------------------------------------------
/test/widget_test.dart:
--------------------------------------------------------------------------------
1 | // This is a basic Flutter widget test.
2 | //
3 | // To perform an interaction with a widget in your test, use the WidgetTester
4 | // utility that Flutter provides. For example, you can send tap and scroll
5 | // gestures. You can also use WidgetTester to find child widgets in the widget
6 | // tree, read text, and verify that the values of widget properties are correct.
7 |
8 | import 'package:flutter/material.dart';
9 | import 'package:flutter_test/flutter_test.dart';
10 |
11 | import 'package:v2manager/main.dart';
12 |
13 | void main() {
14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async {
15 | // Build our app and trigger a frame.
16 | await tester.pumpWidget(const MyApp());
17 |
18 | // Verify that our counter starts at 0.
19 | expect(find.text('0'), findsOneWidget);
20 | expect(find.text('1'), findsNothing);
21 |
22 | // Tap the '+' icon and trigger a frame.
23 | await tester.tap(find.byIcon(Icons.add));
24 | await tester.pump();
25 |
26 | // Verify that our counter has incremented.
27 | expect(find.text('0'), findsNothing);
28 | expect(find.text('1'), findsOneWidget);
29 | });
30 | }
31 |
--------------------------------------------------------------------------------