├── .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 | ![workflow](https://github.com/yatsuki/v2manager/actions/workflows/main.yml/badge.svg) 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 | --------------------------------------------------------------------------------