├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── libs │ │ └── .gitkeep │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── cn │ │ │ │ └── mapleafgo │ │ │ │ └── clash_for_flutter │ │ │ │ ├── BaseService.kt │ │ │ │ ├── ClashService.kt │ │ │ │ └── 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 ├── darwer_img.jpg ├── icon.ico ├── icon.png └── logo_64.png ├── build.yaml ├── distribute_options.yaml ├── docs ├── .nojekyll ├── README.md ├── images │ ├── connect_page.png │ ├── home_page.png │ ├── icon.ico │ ├── log_page.png │ ├── profile_page.png │ ├── proxy_page.png │ └── settings_page.png └── index.html ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── lib ├── app │ ├── app_module.dart │ ├── app_widget.dart │ ├── bean │ │ ├── clash_for_me_config_bean.dart │ │ ├── config_bean.dart │ │ ├── connection_bean.dart │ │ ├── group_bean.dart │ │ ├── history_bean.dart │ │ ├── log_bean.dart │ │ ├── net_speed.dart │ │ ├── profile_base_bean.dart │ │ ├── profile_file_bean.dart │ │ ├── profile_url_bean.dart │ │ ├── provider_bean.dart │ │ ├── proxies_bean.dart │ │ ├── proxy_bean.dart │ │ ├── proxy_providers_bean.dart │ │ ├── sub_userinfo_bean.dart │ │ └── tun_bean.dart │ ├── component │ │ ├── drawer_component.dart │ │ ├── loading_component.dart │ │ └── sys_app_bar.dart │ ├── enum │ │ └── type_enum.dart │ ├── exceptions │ │ └── message_exception.dart │ ├── pages │ │ ├── connections │ │ │ ├── connection_bean.dart │ │ │ ├── connections_module.dart │ │ │ └── connections_page.dart │ │ ├── home │ │ │ ├── home_module.dart │ │ │ └── home_page.dart │ │ ├── index │ │ │ ├── index_module.dart │ │ │ ├── index_page_desktop.dart │ │ │ ├── index_page_mobile.dart │ │ │ ├── init_page.dart │ │ │ └── tray_controller.dart │ │ ├── logs │ │ │ ├── logs_module.dart │ │ │ └── logs_page.dart │ │ ├── profiles │ │ │ ├── profile_item.dart │ │ │ ├── profiles_controller.dart │ │ │ ├── profiles_module.dart │ │ │ └── profiles_page.dart │ │ ├── proxys │ │ │ ├── model │ │ │ │ ├── proxie_show_bean.dart │ │ │ │ ├── proxys_model.dart │ │ │ │ └── proxys_model.g.dart │ │ │ ├── proxys_controller.dart │ │ │ ├── proxys_module.dart │ │ │ └── proxys_page.dart │ │ ├── router.dart │ │ └── settings │ │ │ ├── settings_module.dart │ │ │ └── settings_page.dart │ ├── source │ │ ├── app_config.dart │ │ ├── app_config.g.dart │ │ ├── core_config.dart │ │ ├── core_config.g.dart │ │ ├── logs_subscription.dart │ │ └── request.dart │ └── utils │ │ ├── clash_custom_messages.dart │ │ ├── constants.dart │ │ └── utils.dart ├── clash_generated_bindings.dart ├── core_control.dart ├── main.dart └── main.mapper.g.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── core │ └── .gitkeep ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc ├── my_application.h └── packaging │ └── appimage │ └── make_config.yaml ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── packaging │ └── dmg │ └── make_config.yaml ├── pubspec.lock ├── pubspec.yaml ├── test └── widget_test.dart └── windows ├── .gitignore ├── CMakeLists.txt ├── core └── .gitkeep ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake ├── packaging └── exe │ └── make_config.yaml └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 120 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | build/ 34 | core/dist/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | /appimage-build/ 48 | /AppDir/ 49 | 50 | libclash.* 51 | -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 17 | base_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 18 | - platform: android 19 | create_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 20 | base_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 21 | - platform: ios 22 | create_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 23 | base_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 24 | - platform: linux 25 | create_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 26 | base_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 27 | - platform: macos 28 | create_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 29 | base_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 30 | - platform: windows 31 | create_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 32 | base_revision: e99c9c7cd9f6c0b2f8ae6e3ebfd585239f5568f4 33 | 34 | # User provided section 35 | 36 | # List of Local paths (relative to this file) that should be 37 | # ignored by the migrate tool. 38 | # 39 | # Files that are not part of the templates will be ignored by default. 40 | unmanaged_files: 41 | - 'lib/main.dart' 42 | - 'ios/Runner.xcodeproj/project.pbxproj' 43 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "clash-for-flutter", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "clash-for-flutter (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "clash-for-flutter (release mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "release" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 MapLeafGo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clash for Flutter 2 | 3 | 这是一个 **Clash** 的多平台客户端,支持 windows、linux、macos、android、~~ios~~。 4 | 5 | > [使用说明](https://mapleafgo.github.io/clash-for-flutter) 6 | 7 | ## 界面 8 | 9 | ![主页](./docs/images/home_page.png) 10 | 11 | ![代理页](./docs/images/proxy_page.png) 12 | 13 | ![代理页](./docs/images/log_page.png) 14 | 15 | ![连接页](./docs/images/connect_page.png) 16 | 17 | ![订阅页](./docs/images/profile_page.png) 18 | 19 | ![设置页](./docs/images/settings_page.png) 20 | 21 | ## 开发、打包说明 22 | 23 | - 基础环境 24 | 25 | `Flutter v3.16+` 26 | 27 | > 对目标平台时,需要参照 Flutter 官方文档进行对应平台的环境搭建。如 Android 开发时,需要 Android-SDK 28 | 29 | > `Linux`环境下 [tray_manager](https://github.com/leanflutter/tray_manager) 需要 `libayatana-appindicator3-dev` 30 | 31 | - 下载内核 32 | 33 | 从 https://github.com/mapleafgo/cff-core/releases/latest 下载对应平台需要的内核, 34 | 然后将解压出来的内核文件移动到对应的路径,各平台路径如下: 35 | 36 | ```shell 37 | # windows 38 | windows/core/libclash.dll 39 | # linux 40 | linux/core/libclash.so 41 | # android 42 | android/app/libs/libclash.aar 43 | # macos 44 | macos/Frameworks/libclash.dylib 45 | # ios 46 | ios/Frameworks/libclash.xcframework 47 | ``` 48 | 49 | > 注意:解压出来的文件,仅保留所需的后缀名文件即可,且将其改名为路径的文件名 50 | 51 | > 内核是在 Clash v1.18.0 (非premium) 的基础上进行二次开发的,仅加入了 tun 模式(参照 Meta 进行) 52 | 53 | - 编译项目 54 | 55 | ```shell 56 | # 1. 获取项目依赖 57 | $ flutter pub get 58 | # 2. 生成 .g.dart 文件 59 | $ dart run build_runner build --delete-conflicting-outputs 60 | 61 | # 3. 运行项目 (linux) 62 | $ flutter run -d linux 63 | # 3. 运行项目 (windows) 64 | $ flutter run -d windows 65 | # 3. 运行项目 (android) 66 | $ flutter run -d android 67 | # 3. 运行项目 (macos) 68 | $ flutter run -d macos 69 | ``` 70 | 71 | - 打包项目 72 | 73 | 该项目用 [flutter_distributor](https://distributor.leanflutter.org/) 打包,打包步骤看 `flutter_distributor` 的官方文档吧 74 | 75 | ## 主要技术 76 | 77 | - [Go](https://go.dev/) 78 | - [Clash](https://github.com/Dreamacro/clash) 79 | - [Flutter](https://flutter.dev) 80 | - [tray_manager](https://github.com/leanflutter/tray_manager) 81 | - [window_manager](https://github.com/leanflutter/window_manager) 82 | - [proxy_manager](https://github.com/Kingtous/proxy_manager) 83 | - [flutter_modular](https://github.com/Flutterando/modular) 84 | - [dio](https://github.com/cfug/dio) 85 | - [flutter_distributor](https://distributor.leanflutter.org/) 86 | 87 | ## 写在后面 88 | 89 | 自 1.0.0 版本开始,本软件全面从之前的 Go-Flutter 迁移到了官方 Flutter 90 | 版本。迁移中部分参考了 [Fclash](https://github.com/Kingtous/Fclash) 非常感谢! 91 | -------------------------------------------------------------------------------- /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 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "cn.mapleafgo.clash_for_flutter" 27 | compileSdkVersion flutter.compileSdkVersion 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 | applicationId "cn.mapleafgo.clash_for_flutter" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion 29 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies { 68 | implementation files('libs/libclash.aar') 69 | } 70 | -------------------------------------------------------------------------------- /android/app/libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/android/app/libs/.gitkeep -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 18 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 47 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/cn/mapleafgo/clash_for_flutter/BaseService.kt: -------------------------------------------------------------------------------- 1 | package cn.mapleafgo.clash_for_flutter 2 | 3 | import android.app.NotificationChannel 4 | import android.app.NotificationManager 5 | import android.app.Service 6 | import android.content.Intent 7 | import android.net.VpnService 8 | import android.os.Binder 9 | import android.os.IBinder 10 | import android.os.ParcelFileDescriptor 11 | import androidx.core.app.NotificationCompat 12 | 13 | open class BaseService : VpnService() { 14 | companion object { 15 | val ACTION_CONNECT = "cn.mapleafgo.clash_for_flutter.START" 16 | val ACTION_CLASH = "cn.mapleafgo.clash_for_flutter.CLASH" 17 | val ACTION_DISCONNECT = "cn.mapleafgo.clash_for_flutter.STOP" 18 | } 19 | 20 | private val binder = BaseBinder() 21 | 22 | private val notifyID = 1 23 | private val notifyChannelID = "vpn" 24 | private val notifyChannelName = "VPN服务状态" 25 | private var notificationManager: NotificationManager? = null 26 | 27 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 28 | return START_NOT_STICKY 29 | } 30 | 31 | inner class BaseBinder : Binder() { 32 | fun getService(): BaseService = this@BaseService 33 | } 34 | 35 | override fun onBind(intent: Intent?): IBinder? { 36 | return binder 37 | } 38 | 39 | protected fun startForeground() { 40 | notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager 41 | 42 | val channel = NotificationChannel(notifyChannelID, notifyChannelName, NotificationManager.IMPORTANCE_DEFAULT) 43 | notificationManager?.createNotificationChannel(channel) 44 | 45 | val notification = NotificationCompat.Builder(this, notifyChannelID) 46 | .setSmallIcon(R.mipmap.ic_launcher) 47 | .setContentTitle("Clash for Flutter") 48 | .setContentText("已在后台启动服务") 49 | .setPriority(NotificationCompat.PRIORITY_DEFAULT) 50 | .build() 51 | startForeground(notifyID, notification) 52 | } 53 | 54 | open fun setupVpnServe() { 55 | throw RuntimeException("Stub!") 56 | } 57 | 58 | open fun setupClashServe(): Boolean { 59 | throw RuntimeException("Stub!") 60 | } 61 | 62 | open fun closeVpnService() { 63 | notificationManager?.cancel(notifyID) 64 | } 65 | 66 | override fun onDestroy() { 67 | closeVpnService() 68 | } 69 | 70 | override fun onRevoke() { 71 | closeVpnService() 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/cn/mapleafgo/clash_for_flutter/ClashService.kt: -------------------------------------------------------------------------------- 1 | package cn.mapleafgo.clash_for_flutter 2 | 3 | import android.app.Service 4 | import android.os.ParcelFileDescriptor 5 | import android.util.Log 6 | import cn.mapleafgo.mobile.Mobile 7 | 8 | class ClashService : BaseService() { 9 | private var tun: ParcelFileDescriptor? = null 10 | private var mtu = 1500 11 | private var isRunning = false 12 | 13 | private fun setupTun() { 14 | tun = Builder() 15 | .setMtu(mtu) 16 | .addDnsServer("8.8.8.8") 17 | .addDnsServer("114.114.114.114") 18 | .addRoute("0.0.0.0", 0) 19 | .addAddress("198.18.0.1", 30) 20 | .addDisallowedApplication(packageName) 21 | .establish() ?: throw Exception("启动隧道失败") 22 | } 23 | 24 | override fun setupVpnServe() { 25 | setupTun() 26 | Mobile.operateTun(true, tun?.detachFd()!!, mtu) 27 | Log.i("ClashService", "开启VPN服务") 28 | startForeground() 29 | } 30 | 31 | override fun closeVpnService() { 32 | Mobile.operateTun(false, 0, 0) 33 | tun?.close() 34 | stopForeground(Service.STOP_FOREGROUND_REMOVE) 35 | Log.i("ClashService", "停止VPN服务") 36 | super.closeVpnService() 37 | } 38 | 39 | override fun setupClashServe(): Boolean { 40 | if (!isRunning) { 41 | isRunning = Mobile.startService() 42 | Log.i("ClashService", "开启Clash服务状态: $isRunning") 43 | } 44 | return isRunning 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/cn/mapleafgo/clash_for_flutter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.mapleafgo.clash_for_flutter 2 | 3 | import android.content.ComponentName 4 | import android.content.Intent 5 | import android.content.ServiceConnection 6 | import android.net.VpnService 7 | import android.os.Bundle 8 | import android.os.IBinder 9 | import cn.mapleafgo.mobile.Mobile 10 | import io.flutter.embedding.android.FlutterActivity 11 | import io.flutter.plugin.common.MethodChannel 12 | 13 | class MainActivity : FlutterActivity() { 14 | private val CHANNEL = "cn.mapleafgo/socks_vpn_plugin" 15 | private val REQUEST_CODE = 0 16 | 17 | private lateinit var cService: BaseService 18 | private var cBound: Boolean = false 19 | 20 | private val connection = object : ServiceConnection { 21 | override fun onServiceConnected(className: ComponentName, service: IBinder) { 22 | val binder = service as BaseService.BaseBinder 23 | cService = binder.getService() 24 | cBound = true 25 | } 26 | 27 | override fun onServiceDisconnected(arg0: ComponentName) { 28 | cBound = false 29 | } 30 | } 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | Intent(this, ClashService::class.java).also { intent -> bindService(intent, connection, BIND_AUTO_CREATE) } 35 | MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> 36 | when (call.method) { 37 | "startVpn" -> { 38 | val intent = VpnService.prepare(this) 39 | if (intent != null) { 40 | startActivityForResult(intent, REQUEST_CODE); 41 | result.success(false) 42 | } else { 43 | onActivityResult(REQUEST_CODE, RESULT_OK, null); 44 | result.success(true) 45 | } 46 | } 47 | 48 | "stopVpn" -> { 49 | if (cBound) { 50 | cService.closeVpnService() 51 | } 52 | result.success(cBound) 53 | } 54 | 55 | "startService" -> { 56 | if (cBound) { 57 | val b = cService.setupClashServe() 58 | result.success(b) 59 | } else { 60 | result.success(cBound) 61 | } 62 | } 63 | 64 | "setConfig" -> { 65 | val config = call.argument("config") 66 | result.success(Mobile.setConfig(config)) 67 | } 68 | 69 | "setHomeDir" -> { 70 | val dir = call.argument("dir") 71 | result.success(Mobile.setHomeDir(dir)) 72 | } 73 | 74 | "startRust" -> { 75 | val addr = call.argument("addr") 76 | result.success(Mobile.startRust(addr)) 77 | } 78 | 79 | "verifyMMDB" -> { 80 | val path = call.argument("path") 81 | result.success(Mobile.verifyMMDB(path)) 82 | } 83 | 84 | else -> { 85 | result.notImplemented() 86 | } 87 | } 88 | } 89 | } 90 | 91 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 92 | super.onActivityResult(requestCode, resultCode, data) 93 | if (requestCode == REQUEST_CODE && resultCode == RESULT_OK && cBound) { 94 | cService.setupVpnServe() 95 | } 96 | } 97 | 98 | override fun onDestroy() { 99 | super.onDestroy() 100 | unbindService(connection) 101 | cBound = false 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /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 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | } 24 | subprojects { 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | tasks.register("clean", Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 21 | } 22 | } 23 | 24 | plugins { 25 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 26 | id "com.android.application" version '7.4.2' apply false 27 | } 28 | 29 | include ":app" 30 | -------------------------------------------------------------------------------- /assets/darwer_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/assets/darwer_img.jpg -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/assets/icon.png -------------------------------------------------------------------------------- /assets/logo_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/assets/logo_64.png -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | dart_json_mapper: 5 | generate_for: 6 | - lib/main.dart 7 | 8 | # This part is needed to tell original reflectable builder to stay away 9 | # it overrides default options for reflectable builder to an **empty** set of files 10 | reflectable: 11 | generate_for: 12 | - no/files 13 | -------------------------------------------------------------------------------- /distribute_options.yaml: -------------------------------------------------------------------------------- 1 | output: dist/ 2 | releases: 3 | - name: windows-dev 4 | jobs: 5 | - name: release-dev-windows 6 | package: 7 | platform: windows 8 | target: exe 9 | publish: 10 | target: github 11 | args: 12 | repo-owner: mapleafgo 13 | repo-name: clash-for-flutter 14 | - name: linux-dev 15 | jobs: 16 | - name: release-dev-linux 17 | package: 18 | platform: linux 19 | target: appimage 20 | publish: 21 | target: github 22 | args: 23 | repo-owner: mapleafgo 24 | repo-name: clash-for-flutter 25 | - name: macos-dev 26 | jobs: 27 | - name: release-dev-macos 28 | package: 29 | platform: macos 30 | target: dmg 31 | publish: 32 | target: github 33 | args: 34 | repo-owner: mapleafgo 35 | repo-name: clash-for-flutter 36 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Clash for Flutter 2 | 3 | > 该软件是 Clash 的多平台端实现,使用[Flutter](https://flutter.dev/)开发。支持 Windows、Linux、MacOS、Android、~~IOS~~ 4 | 5 | ### 下载 6 | 7 | 当前下载页面都只提供了 64 位的安装包[releases](https://github.com/mapleafgo/clash-for-flutter/releases/latest) 8 | ,由于当前是我自己手动在各个平台打包,所以做不到每个版本都提供的了安装包 9 | 10 | - Linux: 11 | 12 | linux 安装包目前打包的为`appimage`的,这种包各个发行版都能直接使用 13 | 14 | - Windows: 15 | 16 | windows 提供的是`msi`包 17 | 18 | - MacOS: 19 | 20 | macos 提供的是`dmg`包 21 | 22 | ### 开始使用 23 | 24 | 该软件目前主要操作就三个,订阅、开启代理、切换代理节点。如下 25 | 26 | - #### 订阅 27 | 28 | 订阅页可以说是一切的源头,因为所有的代理节点地址都得通过订阅获取。 29 | 30 | ![profile_page](./images/profile_page.png) 31 | 32 | 这是已有订阅的界面,**在没有订阅或未选择订阅时是无法开启代理的**,添加订阅只需点击右下角的`+` 33 | 按钮。在弹出的窗口中输入订阅地址或选择本地文件 34 | 35 | > 需要说明的是,当前**暂时不未支持自动更新订阅**,需手动在此页更新订阅 36 | 37 | - #### 开启代理 38 | 39 | 在已选择订阅的情况下,只需在本页点击开启即可。 40 | 41 | ![home_page](./images/home_page.png) 42 | 43 | > 当前代理使用的是 PAC 自动设置代理的方式,在各平台通用。这种方式有一定的局限性,因为不是所有程序都会走代理,浏览器是没问题 44 | 45 | - #### 切换代理节点 46 | 47 | ![proxy_page](./images/proxy_page.png) 48 | 49 | 此页就是最常用的页面了,经常使用的 **切换节点、测延迟(右下角按钮)** 都在此页。 50 | 51 | 可能很多人对页面头的几个列表有疑问,这个的话,是订阅配置带过来的。是 Clash 的代理组,它是与规则搭配使用,可以做到对每个 52 | IP、每个地址进行代理配置,具体可查看[Clash 的文档](https://github.com/Dreamacro/clash/wiki/configuration#proxy-groups) 53 | 。这个功能我们软件当前不负责维护,只提供基本的节点切换 54 | 55 | - #### 设置 56 | 57 | ![settings_page](./images/settings_page.png) 58 | 59 | 设置页主要提供切换端口、和几个 Clash 的设置,后期可能会加入软件的一些功能 60 | 61 | - 代理端口 62 | 63 | 对于代理端口,默认为 7890 (设置为 0 表示不开启) 64 | 65 | 至于 Redir 和 Tproxy 的代理,目前我没用到过,都是默认为 0 的状态,这里就引入 Clash 官方的话来解释这两个吧 66 | ```yaml 67 | # Transparent proxy server port for Linux and macOS (Redirect TCP and TProxy UDP) 68 | redir-port: 7892 69 | 70 | # Transparent proxy server port for Linux (TProxy TCP and TProxy UDP) 71 | tproxy-port: 7893 72 | ``` 73 | 74 | - 允许局域网访问 75 | 76 | 这个可以让服务暴露在局域网中,局域网中的其它设备可以接入当前的代理 77 | 78 | - IPv6 79 | 80 | 开启 IPv6 的支持 81 | 82 | ```yaml 83 | # 代理模式 84 | # rule: rule-based packet routing 85 | # global: all packets will be forwarded to a single endpoint 86 | # direct: directly forward the packets to the Internet 87 | mode: rule 88 | 89 | # 输出日志等级 90 | # info / warning / error / debug / silent 91 | log-level: info 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/images/connect_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/docs/images/connect_page.png -------------------------------------------------------------------------------- /docs/images/home_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/docs/images/home_page.png -------------------------------------------------------------------------------- /docs/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/docs/images/icon.ico -------------------------------------------------------------------------------- /docs/images/log_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/docs/images/log_page.png -------------------------------------------------------------------------------- /docs/images/profile_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/docs/images/profile_page.png -------------------------------------------------------------------------------- /docs/images/proxy_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/docs/images/proxy_page.png -------------------------------------------------------------------------------- /docs/images/settings_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/docs/images/settings_page.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Clash for Flutter 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 |
20 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Clash for Flutter 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | clash_for_flutter 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | CFBundleURLTypes 32 | 33 | 34 | CFBundleTypeRole 35 | Editor 36 | CFBundleURLName 37 | 38 | CFBundleURLSchemes 39 | 40 | clash 41 | 42 | 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UISupportedInterfaceOrientations~ipad 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationPortraitUpsideDown 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | CADisableMinimumFrameDurationOnPhone 58 | 59 | UIApplicationSupportsIndirectInputEvents 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/app/app_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/pages/index/index_module.dart'; 2 | import 'package:clash_for_flutter/app/source/app_config.dart'; 3 | import 'package:clash_for_flutter/app/source/core_config.dart'; 4 | import 'package:clash_for_flutter/app/source/logs_subscription.dart'; 5 | import 'package:clash_for_flutter/app/source/request.dart'; 6 | import 'package:flutter_modular/flutter_modular.dart'; 7 | 8 | class AppModule extends Module { 9 | @override 10 | void binds(i) { 11 | i.addSingleton(Request.new); 12 | i.addSingleton(LogsSubscription.new); 13 | i.addSingleton(AppConfig.new); 14 | i.addSingleton(CoreConfig.new); 15 | } 16 | 17 | @override 18 | void routes(r) { 19 | r.module("/", module: IndexModule()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/app/app_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:asuka/asuka.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_localizations/flutter_localizations.dart'; 4 | import 'package:flutter_modular/flutter_modular.dart'; 5 | 6 | class AppWidget extends StatefulWidget { 7 | const AppWidget({super.key}); 8 | 9 | @override 10 | State createState() => _AppWidgetState(); 11 | } 12 | 13 | class _AppWidgetState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | var app = MaterialApp.router( 17 | title: "Clash for Flutter", 18 | localizationsDelegates: GlobalMaterialLocalizations.delegates, 19 | supportedLocales: const [Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN")], 20 | locale: const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN"), 21 | theme: ThemeData( 22 | useMaterial3: true, 23 | primarySwatch: Colors.blue, 24 | ), 25 | routerConfig: Modular.routerConfig, 26 | builder: Asuka.builder, 27 | // navigatorObservers: [Asuka.asukaHeroController], 28 | ); 29 | Modular.setObservers([Asuka.asukaHeroController]); 30 | return app; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/app/bean/clash_for_me_config_bean.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:clash_for_flutter/app/bean/profile_base_bean.dart'; 5 | import 'package:clash_for_flutter/app/utils/constants.dart'; 6 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 7 | 8 | /// 软件配置 9 | @JsonSerializable() 10 | class ClashForMeConfig { 11 | static final File _file = File("${Constants.homeDir.path}${Constants.clashForMe}"); 12 | 13 | /// 选择的配置文件 14 | @JsonProperty(name: "selected-file") 15 | String? selectedFile; 16 | 17 | /// 源配置 18 | List profiles; 19 | 20 | /// mmdb 下载地址 21 | @JsonProperty(name: "mmdb-url") 22 | String mmdbUrl; 23 | 24 | /// 延迟测试地址 25 | @JsonProperty(name: "delay-test-url") 26 | String delayTestUrl; 27 | 28 | /// 是否以 Tun 模式运行 29 | @JsonProperty(name: "tun-if") 30 | bool? tunIf; 31 | 32 | ClashForMeConfig({ 33 | this.selectedFile, 34 | required this.profiles, 35 | required this.mmdbUrl, 36 | required this.delayTestUrl, 37 | this.tunIf, 38 | }); 39 | 40 | ClashForMeConfig copyWith({ 41 | String? selectedFile, 42 | List? profiles, 43 | String? mmdbUrl, 44 | String? delayTestUrl, 45 | bool? tunIf, 46 | }) { 47 | var config = ClashForMeConfig( 48 | selectedFile: selectedFile ?? this.selectedFile, 49 | profiles: profiles ?? this.profiles, 50 | mmdbUrl: mmdbUrl ?? this.mmdbUrl, 51 | delayTestUrl: delayTestUrl ?? this.delayTestUrl, 52 | tunIf: tunIf ?? this.tunIf, 53 | ); 54 | // 对当前选择的订阅进行优化 55 | var selectElements = config.profiles.where((e) => e.file == config.selectedFile); 56 | if (selectElements.isEmpty) { 57 | if (config.profiles.isEmpty) { 58 | config.selectedFile = null; 59 | } else { 60 | config.selectedFile = config.profiles.first.file; 61 | } 62 | } 63 | return config; 64 | } 65 | 66 | Future saveFile() { 67 | return _file.create(recursive: true).then((file) => file.writeAsString(JsonMapper.serialize(this))); 68 | } 69 | 70 | factory ClashForMeConfig.defaultConfig() => ClashForMeConfig( 71 | profiles: [], 72 | mmdbUrl: DefaultConfigValue.mmdbUrl, 73 | delayTestUrl: DefaultConfigValue.delayTestUrl, 74 | ); 75 | 76 | factory ClashForMeConfig.formFile() { 77 | var clashForMeFile = _file; 78 | if (clashForMeFile.existsSync()) { 79 | Map cfm = json.decode(clashForMeFile.readAsStringSync()); 80 | 81 | // 对必填项赋予默认值 82 | cfm.putIfAbsent("mmdb-url", () => DefaultConfigValue.mmdbUrl); 83 | cfm.putIfAbsent("delay-test-url", () => DefaultConfigValue.delayTestUrl); 84 | 85 | return JsonMapper.fromMap(cfm)!; 86 | } 87 | return ClashForMeConfig.defaultConfig(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/app/bean/config_bean.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:clash_for_flutter/app/bean/tun_bean.dart'; 4 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 5 | import 'package:clash_for_flutter/app/utils/constants.dart'; 6 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 7 | import 'package:settings_yaml/settings_yaml.dart'; 8 | 9 | @JsonSerializable() 10 | class Config { 11 | static final String _path = "${Constants.homeDir.path}${Constants.clashConfig}"; 12 | 13 | @JsonProperty(name: "mixed-port") 14 | int? mixedPort; 15 | @JsonProperty(name: "redir-port") 16 | int? redirPort; 17 | @JsonProperty(name: "tproxy-port") 18 | int? tproxyPort; 19 | @JsonProperty(name: "allow-lan") 20 | bool? allowLan; 21 | @JsonProperty(name: "mode") 22 | Mode? mode; 23 | @JsonProperty(name: "log-level") 24 | LogLevel? logLevel; 25 | @JsonProperty(name: "ipv6") 26 | bool? ipv6; 27 | @JsonProperty(name: "tun") 28 | Tun? tun; 29 | 30 | get tunEnable => tun?.enable; 31 | 32 | Config({ 33 | this.mixedPort, 34 | this.redirPort, 35 | this.tproxyPort, 36 | this.allowLan, 37 | this.mode, 38 | this.logLevel, 39 | this.ipv6, 40 | this.tun, 41 | }); 42 | 43 | Future saveFile() { 44 | var yaml = SettingsYaml.load(pathToSettings: _path); 45 | if (redirPort != null) (yaml["redir-port"] = redirPort); 46 | if (tproxyPort != null) (yaml["tproxy-port"] = tproxyPort); 47 | if (mixedPort != null) (yaml["mixed-port"] = mixedPort); 48 | if (allowLan != null) (yaml["allow-lan"] = allowLan); 49 | if (mode != null) (yaml["mode"] = mode?.value); 50 | if (logLevel != null) (yaml["log-level"] = logLevel?.value); 51 | if (ipv6 != null) (yaml["ipv6"] = ipv6); 52 | return yaml.save(); 53 | } 54 | 55 | Config copyWith({ 56 | int? redirPort, 57 | int? tproxyPort, 58 | int? mixedPort, 59 | bool? allowLan, 60 | Mode? mode, 61 | LogLevel? logLevel, 62 | bool? ipv6, 63 | }) { 64 | return Config( 65 | redirPort: redirPort ?? this.redirPort, 66 | tproxyPort: tproxyPort ?? this.tproxyPort, 67 | mixedPort: mixedPort ?? this.mixedPort, 68 | allowLan: allowLan ?? this.allowLan, 69 | mode: mode ?? this.mode, 70 | logLevel: logLevel ?? this.logLevel, 71 | ipv6: ipv6 ?? this.ipv6, 72 | ); 73 | } 74 | 75 | Config copy(Config? that) { 76 | that ??= this; 77 | return Config( 78 | redirPort: that.redirPort, 79 | tproxyPort: that.tproxyPort, 80 | mixedPort: that.mixedPort, 81 | allowLan: that.allowLan, 82 | mode: that.mode, 83 | logLevel: that.logLevel, 84 | ipv6: that.ipv6, 85 | tun: that.tun, 86 | ); 87 | } 88 | 89 | static bool? fileExist() => File(_path).existsSync(); 90 | 91 | factory Config.defaultConfig() => Config(mixedPort: 7890); 92 | } 93 | -------------------------------------------------------------------------------- /lib/app/bean/connection_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 2 | 3 | @JsonSerializable() 4 | class Connection { 5 | String id; 6 | int upload; 7 | int download; 8 | String start; 9 | List chains; 10 | String rule; 11 | String rulePayload; 12 | Metadata metadata; 13 | 14 | Connection({ 15 | required this.id, 16 | required this.upload, 17 | required this.download, 18 | required this.start, 19 | required this.chains, 20 | required this.rule, 21 | required this.rulePayload, 22 | required this.metadata, 23 | }); 24 | 25 | Connection.empty() 26 | : this( 27 | id: "", 28 | upload: 0, 29 | download: 0, 30 | start: "", 31 | chains: [], 32 | rule: "", 33 | rulePayload: "", 34 | metadata: Metadata( 35 | network: "", 36 | type: "", 37 | host: "", 38 | processPath: "", 39 | sourceIP: "", 40 | sourcePort: "", 41 | destinationIP: "", 42 | destinationPort: "", 43 | dnsMode: "", 44 | specialProxy: "", 45 | ), 46 | ); 47 | } 48 | 49 | @JsonSerializable() 50 | class Metadata { 51 | String network; 52 | String type; 53 | String host; 54 | String processPath; 55 | String sourceIP; 56 | String sourcePort; 57 | String destinationIP; 58 | String destinationPort; 59 | String dnsMode; 60 | String specialProxy; 61 | 62 | Metadata({ 63 | required this.network, 64 | required this.type, 65 | required this.host, 66 | required this.processPath, 67 | required this.sourceIP, 68 | required this.sourcePort, 69 | required this.destinationIP, 70 | required this.destinationPort, 71 | required this.dnsMode, 72 | required this.specialProxy, 73 | }); 74 | } 75 | 76 | @JsonSerializable() 77 | class Snapshot { 78 | int uploadTotal; 79 | int downloadTotal; 80 | List connections; 81 | 82 | Snapshot({ 83 | required this.uploadTotal, 84 | required this.downloadTotal, 85 | required this.connections, 86 | }); 87 | 88 | Snapshot.empty() : this(uploadTotal: 0, downloadTotal: 0, connections: []); 89 | } 90 | -------------------------------------------------------------------------------- /lib/app/bean/group_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/bean/history_bean.dart'; 2 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 3 | import '../enum/type_enum.dart'; 4 | 5 | /// 分组代理项 6 | @JsonSerializable() 7 | class Group { 8 | String name; 9 | GroupType type; 10 | List all; 11 | String now; 12 | List? history; 13 | 14 | Group({ 15 | required this.name, 16 | required this.type, 17 | required this.all, 18 | required this.now, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /lib/app/bean/history_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 2 | 3 | @JsonSerializable() 4 | class History { 5 | String time; 6 | int delay; 7 | 8 | History({required this.time, required this.delay}); 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/bean/log_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 2 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 3 | 4 | @JsonSerializable() 5 | class LogData { 6 | DateTime? time; 7 | LogLevel type; 8 | String payload; 9 | 10 | LogData({ 11 | this.time, 12 | required this.type, 13 | required this.payload, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /lib/app/bean/net_speed.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 2 | 3 | @JsonSerializable() 4 | class NetSpeed { 5 | int up = 0; 6 | int down = 0; 7 | } 8 | -------------------------------------------------------------------------------- /lib/app/bean/profile_base_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 2 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 3 | 4 | /// 配置基本参数 5 | @JsonSerializable() 6 | @Json(discriminatorProperty: 'type') 7 | abstract class ProfileBase { 8 | /// 保存的文件名 9 | String file; 10 | 11 | /// 名称 12 | String name; 13 | 14 | /// 配置类型 15 | ProfileType type; 16 | 17 | /// 更新的时间 18 | DateTime time; 19 | 20 | ProfileBase({ 21 | required this.name, 22 | required this.file, 23 | required this.type, 24 | required this.time, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /lib/app/bean/profile_file_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/bean/profile_base_bean.dart'; 2 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 3 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 4 | 5 | /// 配置(文件) 6 | @JsonSerializable() 7 | @Json(discriminatorValue: ProfileType.FILE) 8 | class ProfileFile extends ProfileBase { 9 | /// 链接地址 10 | String? path; 11 | 12 | ProfileFile({ 13 | required super.file, 14 | required super.name, 15 | required super.time, 16 | this.path, 17 | }) : super(type: ProfileType.FILE); 18 | 19 | factory ProfileFile.defaultBean({ 20 | required String file, 21 | required String name, 22 | required DateTime time, 23 | }) => 24 | ProfileFile(file: file, name: name, time: time); 25 | 26 | factory ProfileFile.emptyBean() => 27 | ProfileFile(file: "", name: "", time: DateTime.now()); 28 | } 29 | -------------------------------------------------------------------------------- /lib/app/bean/profile_url_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/bean/profile_base_bean.dart'; 2 | import 'package:clash_for_flutter/app/bean/sub_userinfo_bean.dart'; 3 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 4 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 5 | 6 | /// 配置(URL) 7 | @JsonSerializable() 8 | @Json(discriminatorValue: ProfileType.URL) 9 | class ProfileURL extends ProfileBase { 10 | /// 链接地址 11 | String url; 12 | 13 | /// 更新间隔(h) 14 | int interval; 15 | 16 | @JsonProperty(name: "sub-userinfo") 17 | SubUserinfo? userinfo; 18 | 19 | ProfileURL({ 20 | required super.file, 21 | required super.name, 22 | required super.time, 23 | required this.url, 24 | required this.interval, 25 | this.userinfo, 26 | }) : super(type: ProfileType.URL); 27 | 28 | factory ProfileURL.defaultBean({ 29 | required String url, 30 | required String file, 31 | required String name, 32 | required DateTime time, 33 | }) => 34 | ProfileURL(url: url, file: file, name: name, time: time, interval: 0); 35 | 36 | factory ProfileURL.emptyBean() => ProfileURL( 37 | url: "", file: "", name: "", time: DateTime.now(), interval: 0); 38 | } 39 | -------------------------------------------------------------------------------- /lib/app/bean/provider_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 2 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 3 | import 'group_bean.dart'; 4 | import 'proxy_bean.dart'; 5 | 6 | @JsonSerializable() 7 | class Provider { 8 | String name; 9 | @JsonProperty(converter: ProviderProxiesConverter()) 10 | List proxies; 11 | String type; 12 | VehicleType vehicleType; 13 | String? updatedAt; 14 | 15 | Provider({ 16 | required this.name, 17 | required this.proxies, 18 | required this.type, 19 | required this.vehicleType, 20 | this.updatedAt, 21 | }); 22 | } 23 | 24 | class ProviderProxiesConverter implements ICustomConverter> { 25 | const ProviderProxiesConverter() : super(); 26 | 27 | @override 28 | List fromJSON(jsonValue, [DeserializationContext? context]) { 29 | return jsonValue 30 | .map( 31 | (e) => GroupTypeValue.valueList.contains(e["type"]) 32 | ? JsonMapper.fromMap(e) 33 | : JsonMapper.fromMap(e), 34 | ) 35 | .toList(); 36 | } 37 | 38 | @override 39 | toJSON(List object, [SerializationContext? context]) { 40 | return object.map((e) => e.toJson()).toList(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/app/bean/proxies_bean.dart: -------------------------------------------------------------------------------- 1 | import '../enum/type_enum.dart'; 2 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 3 | 4 | import 'group_bean.dart'; 5 | import 'proxy_bean.dart'; 6 | 7 | @JsonSerializable() 8 | class Proxies { 9 | @JsonProperty(converter: ProxiesConverter()) 10 | Map proxies; 11 | 12 | Proxies({required this.proxies}); 13 | } 14 | 15 | class ProxiesConverter implements ICustomConverter> { 16 | const ProxiesConverter() : super(); 17 | 18 | @override 19 | Map fromJSON(jsonValue, [DeserializationContext? context]) { 20 | return Map.castFrom(jsonValue) 21 | .map((key, e) { 22 | return MapEntry( 23 | key, 24 | GroupTypeValue.valueList.contains(e["type"]) 25 | ? JsonMapper.fromMap(e) 26 | : JsonMapper.fromMap(e), 27 | ); 28 | }); 29 | } 30 | 31 | @override 32 | toJSON(Map object, [SerializationContext? context]) { 33 | return object.map( 34 | (key, value) => MapEntry(key, JsonMapper.serialize(value)), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/app/bean/proxy_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 2 | 3 | import 'history_bean.dart'; 4 | 5 | /// 代理项 6 | @JsonSerializable() 7 | class Proxy { 8 | String name; 9 | String? type; 10 | List? history; 11 | 12 | Proxy({required this.name, this.type}); 13 | } 14 | -------------------------------------------------------------------------------- /lib/app/bean/proxy_providers_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 2 | 3 | import 'provider_bean.dart'; 4 | 5 | @JsonSerializable() 6 | @Json(valueDecorators: ProxyProviders.valueDecorators) 7 | class ProxyProviders { 8 | static Map valueDecorators() => { 9 | typeOf>(): (value) => 10 | Map.castFrom(value), 11 | }; 12 | 13 | Map providers; 14 | 15 | ProxyProviders({required this.providers}); 16 | } 17 | -------------------------------------------------------------------------------- /lib/app/bean/sub_userinfo_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 2 | 3 | /// 订阅信息 4 | @JsonSerializable() 5 | class SubUserinfo { 6 | int? upload; 7 | int? download; 8 | int? total; 9 | int? expire; 10 | 11 | SubUserinfo({ 12 | this.upload, 13 | this.download, 14 | this.total, 15 | this.expire, 16 | }); 17 | 18 | factory SubUserinfo.formHString(String info) { 19 | var list = info.split(";"); 20 | Map map = {}; 21 | for (var i in list) { 22 | var j = i.trim().split("="); 23 | map[j[0]] = int.tryParse(j[1]); 24 | } 25 | return SubUserinfo( 26 | upload: map["upload"], 27 | download: map["download"], 28 | total: map["total"], 29 | expire: map["expire"], 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/app/bean/tun_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 2 | 3 | @JsonSerializable() 4 | class Tun { 5 | bool? enable; 6 | 7 | Tun({ 8 | this.enable, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /lib/app/component/drawer_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:clash_for_flutter/app/bean/net_speed.dart'; 4 | import 'package:clash_for_flutter/app/pages/router.dart'; 5 | import 'package:clash_for_flutter/app/source/request.dart'; 6 | import 'package:clash_for_flutter/app/utils/utils.dart'; 7 | import 'package:easy_sidemenu/easy_sidemenu.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_modular/flutter_modular.dart'; 10 | 11 | class AppDrawer extends StatefulWidget { 12 | final PageController page; 13 | 14 | const AppDrawer({super.key, required this.page}); 15 | 16 | @override 17 | State createState() => _AppDrawerState(); 18 | } 19 | 20 | class _AppDrawerState extends State { 21 | final _request = Modular.get(); 22 | final SideMenuController _smc = SideMenuController(); 23 | NetSpeed _speed = NetSpeed(); 24 | String _clashVersion = "-"; 25 | StreamSubscription? _subscription; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _smc.addListener((p0) => widget.page.jumpToPage(p0)); 31 | _subscription = _request.traffic().listen((event) { 32 | setState(() => _speed = event ?? NetSpeed()); 33 | }); 34 | _request.getClashVersion().then((value) => setState(() => _clashVersion = value ?? _clashVersion)); 35 | } 36 | 37 | @override 38 | void dispose() { 39 | _subscription?.cancel(); 40 | _smc.dispose(); 41 | super.dispose(); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | var themeData = Theme.of(context); 47 | return SideMenu( 48 | controller: _smc, 49 | style: SideMenuStyle( 50 | openSideMenuWidth: 200, 51 | unselectedTitleTextStyle: themeData.textTheme.labelLarge, 52 | selectedTitleTextStyle: themeData.primaryTextTheme.labelLarge, 53 | selectedIconColor: themeData.primaryTextTheme.labelLarge!.color, 54 | selectedColor: themeData.primaryColor, 55 | hoverColor: themeData.hoverColor, 56 | ), 57 | title: Column( 58 | children: [ 59 | ConstrainedBox( 60 | constraints: const BoxConstraints( 61 | maxHeight: 200, 62 | maxWidth: 200, 63 | ), 64 | child: const Image(image: AssetImage("assets/darwer_img.jpg")), 65 | ), 66 | const Divider(indent: 8.0, endIndent: 8.0), 67 | ], 68 | ), 69 | footer: SizedBox( 70 | height: 75, 71 | child: Column( 72 | children: [ 73 | Text( 74 | "↑ ${dataformat(_speed.up)}/s\n↓ ${dataformat(_speed.down)}/s", 75 | style: Theme.of(context).textTheme.titleSmall, 76 | ), 77 | Container( 78 | margin: const EdgeInsets.only(top: 10), 79 | child: Text( 80 | "Clash: $_clashVersion", 81 | style: Theme.of(context).textTheme.labelSmall, 82 | ), 83 | ), 84 | ], 85 | ), 86 | ), 87 | items: menu.sideMenuList, 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/app/component/loading_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Loading { 4 | static OverlayEntry builder() { 5 | return OverlayEntry( 6 | builder: (_) { 7 | return Positioned.fill( 8 | child: Container( 9 | decoration: BoxDecoration( 10 | color: Colors.black.withOpacity(0), 11 | ), 12 | child: Center( 13 | child: Container( 14 | padding: const EdgeInsets.all(30), 15 | decoration: BoxDecoration( 16 | color: Colors.black.withOpacity(0.6), 17 | borderRadius: const BorderRadius.all(Radius.circular(5)), 18 | ), 19 | child: const CircularProgressIndicator(), 20 | ), 21 | ), 22 | ), 23 | ); 24 | }, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/app/component/sys_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:window_manager/window_manager.dart'; 4 | 5 | class SysAppBar extends StatelessWidget implements PreferredSizeWidget { 6 | final double? toolbarHeight; 7 | 8 | final Widget? title; 9 | 10 | final List? actions; 11 | 12 | const SysAppBar({super.key, this.toolbarHeight, this.title, this.actions}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | List acs = []; 17 | if (actions != null) { 18 | acs.addAll(actions!); 19 | } 20 | if (Platform.isWindows || Platform.isLinux) { 21 | acs.add(CloseButton(onPressed: () => windowManager.close())); 22 | } 23 | return GestureDetector( 24 | onPanStart: (_) => windowManager.startDragging(), 25 | child: AppBar( 26 | toolbarHeight: preferredSize.height, 27 | title: title, 28 | actions: acs, 29 | ), 30 | ); 31 | } 32 | 33 | @override 34 | Size get preferredSize => Size.fromHeight(toolbarHeight ?? kToolbarHeight); 35 | } 36 | -------------------------------------------------------------------------------- /lib/app/enum/type_enum.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 2 | 3 | /// 分组类型 4 | @jsonSerializable 5 | enum GroupType { Selector, URLTest, Fallback, LoadBalance } 6 | 7 | extension GroupTypeValue on GroupType { 8 | static List get valueList => GroupType.values.map((e) => e.toString().split(".").last).toList(); 9 | 10 | String get value => GroupTypeValue.valueList[index]; 11 | } 12 | 13 | @jsonSerializable 14 | enum VehicleType { HTTP, File, Compatible } 15 | 16 | extension VehicleTypeValue on VehicleType { 17 | static List get valueList => VehicleType.values.map((e) => e.toString().split(".").last).toList(); 18 | 19 | String get value => VehicleTypeValue.valueList[index]; 20 | } 21 | 22 | /// 使用的选择代理 23 | @jsonSerializable 24 | enum UsedProxy { DIRECT, REJECT, GLOBAL } 25 | 26 | extension UsedProxyValue on UsedProxy { 27 | static List get valueList => UsedProxy.values.map((e) => e.toString().split(".").last).toList(); 28 | 29 | String get value => UsedProxyValue.valueList[index]; 30 | } 31 | 32 | @jsonSerializable 33 | enum Mode { Rule, Global, Direct } 34 | 35 | extension ModeValue on Mode { 36 | static List get valueList => Mode.values.map((e) => e.toString().split(".").last).toList(); 37 | 38 | String get value => ModeValue.valueList[index]; 39 | } 40 | 41 | enum DataUnit { Byte, KB, MB, GB, TB, PB } 42 | 43 | extension DataUnitValue on DataUnit { 44 | static List get valueList => DataUnit.values.map((e) => e.toString().split(".").last).toList(); 45 | 46 | String get value => DataUnitValue.valueList[index]; 47 | } 48 | 49 | /// 排序类型 50 | enum SortType { Default, Name, Delay } 51 | 52 | extension SortTypeShowName on SortType { 53 | static List get showNameList => ["默认", "名称", "延迟"]; 54 | 55 | String get showName => SortTypeShowName.showNameList[index]; 56 | } 57 | 58 | /// 配置类型 59 | @jsonSerializable 60 | enum ProfileType { URL, FILE } 61 | 62 | extension ProfileTypeValue on ProfileType { 63 | static List get valueList => ProfileType.values.map((e) => e.toString().split(".").last).toList(); 64 | 65 | String get value => ProfileTypeValue.valueList[index]; 66 | } 67 | 68 | /// 日志等级 69 | @jsonSerializable 70 | enum LogLevel { debug, info, warning, error, silent } 71 | 72 | extension LogLevelValue on LogLevel { 73 | static List get valueList => LogLevel.values.map((e) => e.toString().split(".").last).toList(); 74 | 75 | String get value => LogLevelValue.valueList[index]; 76 | } 77 | -------------------------------------------------------------------------------- /lib/app/exceptions/message_exception.dart: -------------------------------------------------------------------------------- 1 | /// MessageException 带消息体的异常 2 | class MessageException implements Exception { 3 | late String _message; 4 | 5 | MessageException(String message) { 6 | _message = message; 7 | } 8 | 9 | getMessage() => _message; 10 | } 11 | -------------------------------------------------------------------------------- /lib/app/pages/connections/connection_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 2 | 3 | @JsonSerializable() 4 | class ConnectionShow { 5 | String id; 6 | String host; 7 | String network; 8 | String process; 9 | String type; 10 | String chains; 11 | String rule; 12 | String speed; 13 | String upload; 14 | String download; 15 | String sourceIP; 16 | String time; 17 | 18 | ConnectionShow({ 19 | required this.id, 20 | required this.host, 21 | required this.network, 22 | required this.process, 23 | required this.type, 24 | required this.chains, 25 | required this.rule, 26 | required this.speed, 27 | required this.upload, 28 | required this.download, 29 | required this.sourceIP, 30 | required this.time, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /lib/app/pages/connections/connections_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/pages/connections/connections_page.dart'; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | 4 | class ConnectionsModule extends Module { 5 | @override 6 | void routes(r) { 7 | r.child("/", child: (_) => const ConnectionsPage()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/pages/home/home_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/pages/home/home_page.dart'; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | 4 | class HomeModule extends Module { 5 | @override 6 | void routes(r) { 7 | r.child("/", child: (_) => const HomePage()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/pages/index/index_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/component/sys_app_bar.dart'; 2 | import 'package:clash_for_flutter/app/pages/index/index_page_mobile.dart'; 3 | import 'package:clash_for_flutter/app/pages/index/init_page.dart'; 4 | import 'package:clash_for_flutter/app/pages/index/tray_controller.dart'; 5 | import 'package:clash_for_flutter/app/pages/router.dart'; 6 | import 'package:clash_for_flutter/app/utils/constants.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_modular/flutter_modular.dart'; 9 | 10 | import 'index_page_desktop.dart'; 11 | 12 | class IndexModule extends Module { 13 | @override 14 | List get imports => menu.moduleList; 15 | 16 | @override 17 | void binds(i) { 18 | i.addSingleton(TrayController.new); 19 | } 20 | 21 | @override 22 | void routes(r) { 23 | r.child("/", child: (_) => const InitPage(), children: [ 24 | ChildRoute( 25 | "/error", 26 | child: (_) => const Scaffold( 27 | appBar: SysAppBar(title: Text("Clash for Flutter")), 28 | body: Center(child: Text("初始化失败")), 29 | ), 30 | ), 31 | ]); 32 | r.child("/tab", child: (_) { 33 | return Constants.isDesktop ? const IndexDesktopPage() : const IndexMobilePage(); 34 | }, children: menu.routes); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/app/pages/index/index_page_desktop.dart: -------------------------------------------------------------------------------- 1 | import 'package:asuka/asuka.dart'; 2 | import 'package:clash_for_flutter/app/bean/profile_url_bean.dart'; 3 | import 'package:clash_for_flutter/app/component/drawer_component.dart'; 4 | import 'package:clash_for_flutter/app/component/loading_component.dart'; 5 | import 'package:clash_for_flutter/app/pages/index/tray_controller.dart'; 6 | import 'package:clash_for_flutter/app/pages/router.dart'; 7 | import 'package:clash_for_flutter/app/source/app_config.dart'; 8 | import 'package:clash_for_flutter/app/source/request.dart'; 9 | import 'package:desktop_lifecycle/desktop_lifecycle.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_modular/flutter_modular.dart'; 12 | import 'package:protocol_handler/protocol_handler.dart'; 13 | import 'package:window_manager/window_manager.dart'; 14 | 15 | class IndexDesktopPage extends StatefulWidget { 16 | const IndexDesktopPage({super.key}); 17 | 18 | @override 19 | State createState() => _IndexDesktopPageState(); 20 | } 21 | 22 | class _IndexDesktopPageState extends State 23 | with WindowListener, ProtocolListener, WidgetsBindingObserver { 24 | final _config = Modular.get(); 25 | final _request = Modular.get(); 26 | final _tray = Modular.get(); 27 | final _lifeEvent = DesktopLifecycle.instance.isActive; 28 | 29 | final PageController _page = PageController(); 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | // 窗口监听 35 | windowManager.addListener(this); 36 | // 协议监听 37 | protocolHandler.addListener(this); 38 | // 移动端前后台监听 39 | WidgetsBinding.instance.addObserver(this); 40 | // 桌面端前后台监听 41 | _lifeEvent.addListener(() => appListener(_lifeEvent.value)); 42 | // 接管窗口的关闭按钮 43 | windowManager.setPreventClose(true); 44 | // 托盘初始化 45 | _tray.init(); 46 | Modular.to.navigate("/tab/home/"); 47 | } 48 | 49 | @override 50 | void dispose() { 51 | _tray.dispose(); 52 | windowManager.removeListener(this); 53 | protocolHandler.removeListener(this); 54 | WidgetsBinding.instance.removeObserver(this); 55 | super.dispose(); 56 | } 57 | 58 | @override 59 | void onWindowClose() async { 60 | if (await windowManager.isPreventClose()) { 61 | windowManager.hide(); 62 | } 63 | } 64 | 65 | @override 66 | void onWindowFocus() { 67 | setState(() {}); 68 | } 69 | 70 | /// 处理在移动端前后台 71 | @override 72 | void didChangeAppLifecycleState(AppLifecycleState state) { 73 | appListener(state == AppLifecycleState.inactive); 74 | } 75 | 76 | /// 统一处理前后台改变 77 | void appListener(bool state) { 78 | if (state) { 79 | print("应用前台"); 80 | } else { 81 | print("应用后台"); 82 | } 83 | } 84 | 85 | /// 外链接 86 | @override 87 | void onProtocolUrlReceived(String url) { 88 | var uri = Uri.parse(Uri.decodeFull(url)); 89 | // 导入订阅 90 | if (uri.host == "install-config") { 91 | var params = uri.queryParameters; 92 | var subscribeUrl = params["url"]; 93 | if (subscribeUrl != null) { 94 | var profile = ProfileURL.emptyBean() 95 | ..url = subscribeUrl 96 | ..name = params["name"] ?? ""; 97 | 98 | var loading = Loading.builder(); 99 | Asuka.addOverlay(loading); 100 | _request.getSubscribe(profile: profile, profilesDir: _config.profilesPath).then((p) { 101 | var tempList = _config.profiles.toList(); 102 | tempList.add(p); 103 | _config.setState(profiles: tempList); 104 | Asuka.showSnackBar(const SnackBar(content: Text("导入成功"))); 105 | }).catchError((e) { 106 | Asuka.showSnackBar(SnackBar(content: Text("导入异常: $e"))); 107 | }).then((_) { 108 | loading.remove(); 109 | _page.jumpToPage(2); 110 | }); 111 | } else { 112 | Asuka.showSnackBar(const SnackBar(content: Text("导入订阅链接有误"))); 113 | } 114 | } 115 | } 116 | 117 | @override 118 | Widget build(BuildContext context) { 119 | return Row(children: [ 120 | AppDrawer(page: _page), 121 | Expanded( 122 | child: PageView.builder( 123 | controller: _page, 124 | itemCount: menu.size, 125 | onPageChanged: (i) => Modular.to.navigate("/tab${menu.getPath(i)}/"), 126 | itemBuilder: (_, __) => const RouterOutlet(), 127 | ), 128 | ) 129 | ]); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/app/pages/index/index_page_mobile.dart: -------------------------------------------------------------------------------- 1 | import 'package:asuka/asuka.dart'; 2 | import 'package:clash_for_flutter/app/bean/profile_url_bean.dart'; 3 | import 'package:clash_for_flutter/app/component/drawer_component.dart'; 4 | import 'package:clash_for_flutter/app/component/loading_component.dart'; 5 | import 'package:clash_for_flutter/app/pages/router.dart'; 6 | import 'package:clash_for_flutter/app/source/app_config.dart'; 7 | import 'package:clash_for_flutter/app/source/request.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_modular/flutter_modular.dart'; 10 | import 'package:protocol_handler/protocol_handler.dart'; 11 | 12 | class IndexMobilePage extends StatefulWidget { 13 | const IndexMobilePage({super.key}); 14 | 15 | @override 16 | State createState() => _IndexPageState(); 17 | } 18 | 19 | class _IndexPageState extends State with ProtocolListener, WidgetsBindingObserver { 20 | final _config = Modular.get(); 21 | final _request = Modular.get(); 22 | 23 | final PageController _page = PageController(); 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | // 协议监听 29 | protocolHandler.addListener(this); 30 | // 移动端前后台监听 31 | WidgetsBinding.instance.addObserver(this); 32 | Modular.to.navigate("/tab/home/"); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | protocolHandler.removeListener(this); 38 | WidgetsBinding.instance.removeObserver(this); 39 | super.dispose(); 40 | } 41 | 42 | /// 处理在移动端前后台 43 | @override 44 | void didChangeAppLifecycleState(AppLifecycleState state) { 45 | appListener(state == AppLifecycleState.inactive); 46 | } 47 | 48 | /// 统一处理前后台改变 49 | void appListener(bool state) { 50 | if (state) { 51 | print("应用前台"); 52 | } else { 53 | print("应用后台"); 54 | } 55 | } 56 | 57 | /// 外链接 58 | @override 59 | void onProtocolUrlReceived(String url) { 60 | var uri = Uri.parse(Uri.decodeFull(url)); 61 | // 导入订阅 62 | if (uri.host == "install-config") { 63 | var params = uri.queryParameters; 64 | var subscribeUrl = params["url"]; 65 | if (subscribeUrl != null) { 66 | var profile = ProfileURL.emptyBean() 67 | ..url = subscribeUrl 68 | ..name = params["name"] ?? ""; 69 | 70 | var loading = Loading.builder(); 71 | Asuka.addOverlay(loading); 72 | _request.getSubscribe(profile: profile, profilesDir: _config.profilesPath).then((p) { 73 | var tempList = _config.profiles.toList(); 74 | tempList.add(p); 75 | _config.setState(profiles: tempList); 76 | Asuka.showSnackBar(const SnackBar(content: Text("导入成功"))); 77 | }).catchError((e) { 78 | Asuka.showSnackBar(SnackBar(content: Text("导入异常: $e"))); 79 | }).then((_) { 80 | loading.remove(); 81 | _page.jumpToPage(2); 82 | }); 83 | } else { 84 | Asuka.showSnackBar(const SnackBar(content: Text("导入订阅链接有误"))); 85 | } 86 | } 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | return Row(children: [ 92 | AppDrawer(page: _page), 93 | Expanded( 94 | child: PageView.builder( 95 | controller: _page, 96 | itemCount: menu.size, 97 | onPageChanged: (i) => Modular.to.navigate("/tab${menu.getPath(i)}/"), 98 | itemBuilder: (_, __) => const RouterOutlet(), 99 | ), 100 | ) 101 | ]); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/app/pages/index/init_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:asuka/asuka.dart'; 4 | import 'package:clash_for_flutter/app/component/sys_app_bar.dart'; 5 | import 'package:clash_for_flutter/app/exceptions/message_exception.dart'; 6 | import 'package:clash_for_flutter/app/source/app_config.dart'; 7 | import 'package:clash_for_flutter/app/source/core_config.dart'; 8 | import 'package:clash_for_flutter/app/source/logs_subscription.dart'; 9 | import 'package:clash_for_flutter/app/source/request.dart'; 10 | import 'package:clash_for_flutter/app/utils/constants.dart'; 11 | import 'package:clash_for_flutter/core_control.dart'; 12 | import 'package:flutter/material.dart'; 13 | import 'package:flutter_modular/flutter_modular.dart'; 14 | 15 | class InitPage extends StatefulWidget { 16 | const InitPage({super.key}); 17 | 18 | @override 19 | State createState() => _InitPageState(); 20 | } 21 | 22 | class _InitPageState extends State { 23 | final _config = Modular.get(); 24 | final _core = Modular.get(); 25 | final _request = Modular.get(); 26 | final _logs = Modular.get(); 27 | double _loadingProgress = 0; 28 | bool _isLoading = false; 29 | 30 | @override 31 | void initState() { 32 | _init(); 33 | super.initState(); 34 | } 35 | 36 | _init() { 37 | return Future(() async { 38 | if (!await _request.hello().then((res) => res.statusCode == HttpStatus.ok)) { 39 | throw MessageException("无法连接到内核,请尝试重启应用"); 40 | } 41 | 42 | _core.init(); 43 | await _config.init(); 44 | 45 | var m = File("${Constants.homeDir.path}${Constants.mmdb}"); 46 | if (!(await CoreControl.verifyMMDB(m.path) ?? false)) { 47 | setState(() => _isLoading = true); 48 | await _request 49 | .downFile( 50 | urlPath: _config.clashForMe.mmdbUrl, 51 | savePath: m.path, 52 | onReceiveProgress: (received, total) { 53 | setState(() => _loadingProgress = received / total); 54 | }, 55 | ) 56 | .then((value) => setState(() => _isLoading = false)); 57 | } 58 | 59 | await _core.asyncConfig(); 60 | 61 | // 已经开启tun直接跳转 62 | if (_config.tunIf && _core.tunEnable) { 63 | return; 64 | } 65 | 66 | // 同步当前 profile 67 | if (await _config.asyncProfile()) { 68 | return; 69 | } 70 | }).then((value) async { 71 | await _core.asyncConfig(); 72 | _logs.startSubLogs(); // 启动日志订阅 73 | Modular.to.navigate("/tab"); 74 | }).onError((error, stackTrace) { 75 | Modular.to.navigate("/error"); 76 | Asuka.showSnackBar(SnackBar(content: Text(error.toString()))); 77 | }); 78 | } 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | return _isLoading ? LoadingWidget(value: _loadingProgress) : const RouterOutlet(); 83 | } 84 | } 85 | 86 | class LoadingWidget extends StatelessWidget { 87 | const LoadingWidget({super.key, required this.value}); 88 | 89 | final double value; 90 | 91 | @override 92 | Widget build(BuildContext context) { 93 | var size = MediaQuery.of(context).size; 94 | return Scaffold( 95 | appBar: const SysAppBar(title: Text("Clash for Flutter")), 96 | body: Center( 97 | child: SizedBox( 98 | height: 200, 99 | child: Flex( 100 | direction: Axis.vertical, 101 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 102 | children: [ 103 | SizedBox( 104 | width: size.width * 0.6, 105 | child: LinearProgressIndicator( 106 | value: value, 107 | backgroundColor: Colors.black12, 108 | minHeight: 10, 109 | ), 110 | ), 111 | const Text("正在初始下载 Country.mmdb 文件"), 112 | ], 113 | ), 114 | ), 115 | ), 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/app/pages/index/tray_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 4 | import 'package:clash_for_flutter/app/source/app_config.dart'; 5 | import 'package:clash_for_flutter/app/source/core_config.dart'; 6 | import 'package:flutter_modular/flutter_modular.dart'; 7 | import 'package:mobx/mobx.dart'; 8 | import 'package:tray_manager/tray_manager.dart'; 9 | import 'package:window_manager/window_manager.dart'; 10 | 11 | /// 托盘菜单控制类 12 | class TrayController extends Disposable with TrayListener { 13 | final _config = Modular.get(); 14 | final _core = Modular.get(); 15 | 16 | Future init() async { 17 | trayManager.addListener(this); 18 | // 监听系统代理 19 | reaction( 20 | (_) => _config.systemProxy, 21 | (status) { 22 | _menuReset(isChecked: status, mode: _core.clash.mode ?? Mode.Rule); 23 | }, 24 | ); 25 | // 监听代理模式 26 | reaction( 27 | (_) => _core.clash.mode, 28 | (mode) { 29 | _menuReset(isChecked: _config.systemProxy, mode: mode ?? Mode.Rule); 30 | }, 31 | ); 32 | await trayManager.setIcon( 33 | Platform.isWindows ? 'assets/icon.ico' : 'assets/logo_64.png', 34 | ); 35 | // 初始化托盘菜单 36 | await _menuReset( 37 | isChecked: _config.systemProxy, 38 | mode: _core.clash.mode ?? Mode.Rule, 39 | ); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | trayManager.removeListener(this); 45 | } 46 | 47 | Future _menuReset({required bool isChecked, required Mode mode}) async { 48 | final Menu menu = Menu( 49 | items: [ 50 | MenuItem( 51 | key: 'show_window', 52 | label: '显示窗口', 53 | ), 54 | MenuItem.separator(), 55 | MenuItem.checkbox(key: 'proxy', label: '代理', checked: isChecked), 56 | MenuItem.submenu( 57 | label: '模式', 58 | submenu: Menu( 59 | items: [ 60 | MenuItem.checkbox( 61 | key: Mode.Rule.value, 62 | label: Mode.Rule.value, 63 | checked: mode == Mode.Rule, 64 | ), 65 | MenuItem.checkbox( 66 | key: Mode.Global.value, 67 | label: Mode.Global.value, 68 | checked: mode == Mode.Global, 69 | ), 70 | MenuItem.checkbox( 71 | key: Mode.Direct.value, 72 | label: Mode.Direct.value, 73 | checked: mode == Mode.Direct, 74 | ), 75 | ], 76 | ), 77 | ), 78 | MenuItem( 79 | label: '退出', 80 | key: 'exit_app', 81 | ) 82 | ], 83 | ); 84 | await trayManager.setContextMenu(menu); 85 | } 86 | 87 | @override 88 | void onTrayIconMouseDown() { 89 | windowManager.show(); 90 | } 91 | 92 | @override 93 | void onTrayIconRightMouseDown() { 94 | trayManager.popUpContextMenu(); 95 | } 96 | 97 | @override 98 | void onTrayMenuItemClick(MenuItem menuItem) async { 99 | if (menuItem.key == 'show_window') { 100 | windowManager.show(); 101 | } else if (menuItem.key == 'proxy') { 102 | if (menuItem.checked ?? false) { 103 | await _config.closeProxy(); 104 | } else { 105 | await _config.openProxy(); 106 | } 107 | } else if (menuItem.key == 'exit_app') { 108 | await _config.closeProxy(); 109 | windowManager.close().then((_) => windowManager.destroy()); 110 | } else if (menuItem.key == Mode.Rule.value) { 111 | _core.setState(mode: Mode.Rule); 112 | } else if (menuItem.key == Mode.Global.value) { 113 | _core.setState(mode: Mode.Global); 114 | } else if (menuItem.key == Mode.Direct.value) { 115 | _core.setState(mode: Mode.Direct); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/app/pages/logs/logs_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/pages/logs/logs_page.dart'; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | 4 | class LogsModule extends Module { 5 | @override 6 | void routes(r) { 7 | r.child("/", child: (_) => const LogsPage()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/pages/profiles/profiles_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:asuka/asuka.dart'; 4 | import 'package:clash_for_flutter/app/bean/profile_base_bean.dart'; 5 | import 'package:clash_for_flutter/app/bean/profile_file_bean.dart'; 6 | import 'package:clash_for_flutter/app/bean/profile_url_bean.dart'; 7 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 8 | import 'package:clash_for_flutter/app/source/app_config.dart'; 9 | import 'package:clash_for_flutter/app/source/request.dart'; 10 | import 'package:clash_for_flutter/app/utils/constants.dart'; 11 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 12 | import 'package:flutter/material.dart'; 13 | import 'package:flutter_modular/flutter_modular.dart'; 14 | 15 | class ProfileShow { 16 | final String title; 17 | final ProfileType type; 18 | int? use; 19 | int? total; 20 | DateTime? expire; 21 | final DateTime lastUpdate; 22 | 23 | ProfileShow({ 24 | required this.title, 25 | required this.type, 26 | this.use, 27 | this.total, 28 | this.expire, 29 | required this.lastUpdate, 30 | }); 31 | } 32 | 33 | class ProfileController { 34 | final _request = Modular.get(); 35 | final _config = Modular.get(); 36 | 37 | Future addProfile(ProfileBase profile) { 38 | Future handle; 39 | switch (profile.type) { 40 | case ProfileType.URL: 41 | handle = _request.getSubscribe( 42 | profile: profile as ProfileURL, 43 | profilesDir: _config.profilesPath, 44 | ); 45 | break; 46 | case ProfileType.FILE: 47 | var time = DateTime.now(); 48 | var file = "${time.millisecondsSinceEpoch}.yaml"; 49 | var savePath = "${_config.profilesPath}/$file"; 50 | handle = File((profile as ProfileFile).path!).copy(savePath).then((_) { 51 | return profile 52 | ..time = time 53 | ..file = file; 54 | }); 55 | break; 56 | default: 57 | return Future(() => null); 58 | } 59 | 60 | return handle.then((p) { 61 | var tempList = _config.profiles.toList(); 62 | tempList.add(p); 63 | _config.setState(profiles: tempList); 64 | }); 65 | } 66 | 67 | /// 选择某源 68 | select(String file) { 69 | _config.setState(selectedFile: file); 70 | } 71 | 72 | /// 编辑源 73 | edit(ProfileBase profile) { 74 | var tempList = _config.profiles.toList(); 75 | var i = tempList.indexWhere((element) => element.file == profile.file); 76 | tempList[i] = profile; 77 | _config.setState(profiles: tempList); 78 | } 79 | 80 | /// 移除源 81 | void removeProfile(String file) { 82 | var isActive = _config.selectedFile == file; 83 | var tempList = _config.profiles.toList(); 84 | tempList.removeWhere((e) => e.file == file); 85 | if (isActive && tempList.isNotEmpty) { 86 | _config.setState(selectedFile: tempList.first.file, profiles: tempList); 87 | } else { 88 | _config.setState(profiles: tempList); 89 | } 90 | File("${Constants.homeDir.path}${Constants.profilesPath}/$file").delete(); 91 | } 92 | 93 | /// 更新源(仅限URL) 94 | Future updateProfile(String file) async { 95 | var tempList = _config.profiles.toList(); 96 | var index = tempList.indexWhere((e) => e.file == file); 97 | 98 | var profile = JsonMapper.clone(tempList[index] as ProfileURL); 99 | if (profile == null) return; 100 | 101 | try { 102 | profile = await _request.getSubscribe( 103 | profile: profile, 104 | profilesDir: _config.profilesPath, 105 | ); 106 | } catch (e) { 107 | Asuka.showSnackBar(SnackBar(content: Text("更新异常: $e"))); 108 | return; 109 | } 110 | 111 | tempList.replaceRange(index, index + 1, [profile]); 112 | 113 | if (_config.selectedFile == file) { 114 | _config.setState(selectedFile: profile.file, profiles: tempList); 115 | } else { 116 | _config.setState(profiles: tempList); 117 | } 118 | File("${Constants.homeDir.path}${Constants.profilesPath}/$file").delete(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/app/pages/profiles/profiles_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/pages/profiles/profiles_controller.dart'; 2 | import 'package:clash_for_flutter/app/pages/profiles/profiles_page.dart'; 3 | import 'package:flutter_modular/flutter_modular.dart'; 4 | 5 | class ProfilesModule extends Module { 6 | @override 7 | void binds(i) { 8 | i.add(ProfileController.new); 9 | } 10 | 11 | @override 12 | void routes(r) { 13 | r.child("/", child: (_) => const ProfilesPage()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/app/pages/proxys/model/proxie_show_bean.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 2 | 3 | /// 仅用于显示的代理项 4 | @JsonSerializable() 5 | class ProxieShow { 6 | String name; 7 | String subTitle; 8 | int delay; 9 | 10 | ProxieShow({ 11 | required this.name, 12 | required this.delay, 13 | required this.subTitle, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /lib/app/pages/proxys/model/proxys_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/bean/group_bean.dart'; 2 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'proxys_model.g.dart'; 6 | 7 | class ProxysModel = ProxysModelBase with _$ProxysModel; 8 | 9 | /// 代理列表 10 | abstract class ProxysModelBase with Store { 11 | /// 分组列表 12 | @observable 13 | var groups = []; 14 | @observable 15 | Group? global; 16 | @observable 17 | SortType sortType = SortType.Default; 18 | 19 | /// 代理列表 20 | @observable 21 | Map proxiesMap = {}; 22 | 23 | @action 24 | setState({ 25 | Group? global, 26 | SortType? sortType, 27 | List? groups, 28 | Map? proxiesMap, 29 | }) { 30 | if (global != null) this.global = global; 31 | if (sortType != null) this.sortType = sortType; 32 | if (groups != null) this.groups = groups; 33 | if (proxiesMap != null) this.proxiesMap = proxiesMap; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/app/pages/proxys/model/proxys_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'proxys_model.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers 10 | 11 | mixin _$ProxysModel on ProxysModelBase, Store { 12 | late final _$groupsAtom = 13 | Atom(name: 'ProxysModelBase.groups', context: context); 14 | 15 | @override 16 | List get groups { 17 | _$groupsAtom.reportRead(); 18 | return super.groups; 19 | } 20 | 21 | @override 22 | set groups(List value) { 23 | _$groupsAtom.reportWrite(value, super.groups, () { 24 | super.groups = value; 25 | }); 26 | } 27 | 28 | late final _$globalAtom = 29 | Atom(name: 'ProxysModelBase.global', context: context); 30 | 31 | @override 32 | Group? get global { 33 | _$globalAtom.reportRead(); 34 | return super.global; 35 | } 36 | 37 | @override 38 | set global(Group? value) { 39 | _$globalAtom.reportWrite(value, super.global, () { 40 | super.global = value; 41 | }); 42 | } 43 | 44 | late final _$sortTypeAtom = 45 | Atom(name: 'ProxysModelBase.sortType', context: context); 46 | 47 | @override 48 | SortType get sortType { 49 | _$sortTypeAtom.reportRead(); 50 | return super.sortType; 51 | } 52 | 53 | @override 54 | set sortType(SortType value) { 55 | _$sortTypeAtom.reportWrite(value, super.sortType, () { 56 | super.sortType = value; 57 | }); 58 | } 59 | 60 | late final _$proxiesMapAtom = 61 | Atom(name: 'ProxysModelBase.proxiesMap', context: context); 62 | 63 | @override 64 | Map get proxiesMap { 65 | _$proxiesMapAtom.reportRead(); 66 | return super.proxiesMap; 67 | } 68 | 69 | @override 70 | set proxiesMap(Map value) { 71 | _$proxiesMapAtom.reportWrite(value, super.proxiesMap, () { 72 | super.proxiesMap = value; 73 | }); 74 | } 75 | 76 | late final _$ProxysModelBaseActionController = 77 | ActionController(name: 'ProxysModelBase', context: context); 78 | 79 | @override 80 | dynamic setState( 81 | {Group? global, 82 | SortType? sortType, 83 | List? groups, 84 | Map? proxiesMap}) { 85 | final _$actionInfo = _$ProxysModelBaseActionController.startAction( 86 | name: 'ProxysModelBase.setState'); 87 | try { 88 | return super.setState( 89 | global: global, 90 | sortType: sortType, 91 | groups: groups, 92 | proxiesMap: proxiesMap); 93 | } finally { 94 | _$ProxysModelBaseActionController.endAction(_$actionInfo); 95 | } 96 | } 97 | 98 | @override 99 | String toString() { 100 | return ''' 101 | groups: ${groups}, 102 | global: ${global}, 103 | sortType: ${sortType}, 104 | proxiesMap: ${proxiesMap} 105 | '''; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/app/pages/proxys/proxys_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:clash_for_flutter/app/bean/group_bean.dart'; 4 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 5 | import 'package:clash_for_flutter/app/pages/proxys/model/proxie_show_bean.dart'; 6 | import 'package:clash_for_flutter/app/pages/proxys/model/proxys_model.dart'; 7 | import 'package:clash_for_flutter/app/source/core_config.dart'; 8 | import 'package:clash_for_flutter/app/source/app_config.dart'; 9 | import 'package:clash_for_flutter/app/source/request.dart'; 10 | import 'package:flutter_modular/flutter_modular.dart'; 11 | 12 | class ProxysController { 13 | final _request = Modular.get(); 14 | final _config = Modular.get(); 15 | final _core = Modular.get(); 16 | final model = Modular.get(); 17 | 18 | Future initState() async { 19 | await getProxies(); 20 | } 21 | 22 | Future getProxies() async { 23 | var proxies = await _request.getProxies(); 24 | var global = proxies?.proxies[UsedProxy.GLOBAL.value] as Group; 25 | 26 | var list = global.all 27 | .where((name) => !UsedProxyValue.valueList.contains(name)) 28 | .map((key) => proxies?.proxies[key]) 29 | .toList(); 30 | 31 | List groupList = []; 32 | Map proxiesMap = {}; 33 | for (var item in list) { 34 | proxiesMap[item.name] = item; 35 | if (item is Group && item.type == GroupType.Selector) { // 可选的分组 36 | groupList.add(item); 37 | } 38 | } 39 | 40 | // 全局代理时 41 | if (_core.clash.mode == Mode.Global) { 42 | groupList = [global, ...groupList]; 43 | } 44 | 45 | model.setState(groups: groupList, proxiesMap: proxiesMap, global: global); 46 | } 47 | 48 | Future select({ 49 | required String name, 50 | required String select, 51 | }) async { 52 | await _request.changeProxy(name: name, select: select); 53 | await getProxies(); 54 | } 55 | 56 | Future delayGroup(Group group) { 57 | final delayTestUrl = _config.clashForMe.delayTestUrl; 58 | return Future.wait( 59 | group.all.map((name) => _request.getProxyDelay(name, delayTestUrl).catchError((_) => 0)), 60 | ).then((_) => getProxies()); 61 | } 62 | 63 | void sort(SortType type) { 64 | model.setState(sortType: type); 65 | } 66 | 67 | List getShowList(Group group) { 68 | var list = group.all.map((e) => _proxieShow(e)).toList(); 69 | switch (model.sortType) { 70 | case SortType.Delay: 71 | if (list.any((element) => element.delay > 0)) { 72 | list.sort((a, b) { 73 | if (a.delay < 1 || b.delay < 1) { 74 | return b.delay.compareTo(a.delay); 75 | } 76 | return a.delay.compareTo(b.delay); 77 | }); 78 | } 79 | break; 80 | case SortType.Name: 81 | list.sort((a, b) => a.name.compareTo(b.name)); 82 | break; 83 | case SortType.Default: 84 | break; 85 | } 86 | return list; 87 | } 88 | 89 | ProxieShow _proxieShow(String name) { 90 | var proxie = model.proxiesMap[name]; 91 | if (proxie == null) { 92 | return ProxieShow(name: name, subTitle: "", delay: -1); 93 | } 94 | var historys = proxie.history ?? []; 95 | if (proxie is Group) { 96 | var subTitle = StringBuffer(); 97 | subTitle.write(proxie.type.value); 98 | subTitle.write(" [${proxie.now}]"); 99 | return ProxieShow( 100 | name: proxie.name, 101 | subTitle: subTitle.toString(), 102 | delay: historys.isNotEmpty ? historys.last.delay : -1, 103 | ); 104 | } else { 105 | return ProxieShow( 106 | name: proxie.name, 107 | subTitle: proxie.type, 108 | delay: historys.isNotEmpty ? historys.last.delay : -1, 109 | ); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/app/pages/proxys/proxys_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/pages/proxys/model/proxys_model.dart'; 2 | import 'package:clash_for_flutter/app/pages/proxys/proxys_controller.dart'; 3 | import 'package:clash_for_flutter/app/pages/proxys/proxys_page.dart'; 4 | import 'package:flutter_modular/flutter_modular.dart'; 5 | 6 | class ProxysModule extends Module { 7 | @override 8 | void binds(i) { 9 | i.addSingleton(ProxysModel.new); 10 | i.add(ProxysController.new); 11 | } 12 | 13 | @override 14 | void routes(r) { 15 | r.child("/", child: (_) => const ProxysPage()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/app/pages/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/pages/connections/connections_module.dart'; 2 | import 'package:clash_for_flutter/app/pages/home/home_module.dart'; 3 | import 'package:clash_for_flutter/app/pages/logs/logs_module.dart'; 4 | import 'package:clash_for_flutter/app/pages/profiles/profiles_module.dart'; 5 | import 'package:clash_for_flutter/app/pages/proxys/proxys_module.dart'; 6 | import 'package:clash_for_flutter/app/pages/settings/settings_module.dart'; 7 | import 'package:easy_sidemenu/easy_sidemenu.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_modular/flutter_modular.dart'; 10 | 11 | class MenuRouteItem { 12 | final String title; 13 | final Icon icon; 14 | final String path; 15 | final Module module; 16 | 17 | const MenuRouteItem({ 18 | required this.title, 19 | required this.icon, 20 | required this.path, 21 | required this.module, 22 | }); 23 | } 24 | 25 | class MenuRoute { 26 | final List menuList; 27 | 28 | const MenuRoute(this.menuList); 29 | 30 | int get size => menuList.length; 31 | 32 | List get moduleList { 33 | return menuList.map((e) => e.module).toList(); 34 | } 35 | 36 | List get routes { 37 | return menuList.map((e) => ModuleRoute(e.path, module: e.module)).toList(); 38 | } 39 | 40 | List get sideMenuList { 41 | return menuList.map((e) => SideMenuItem(title: e.title, icon: e.icon, onTap: (i, c) => c.changePage(i))).toList(); 42 | } 43 | 44 | getPath(int index) { 45 | return menuList[index].path; 46 | } 47 | } 48 | 49 | final MenuRoute menu = MenuRoute([ 50 | MenuRouteItem( 51 | title: "首页", 52 | icon: const Icon(Icons.home_outlined), 53 | path: "/home", 54 | module: HomeModule(), 55 | ), 56 | MenuRouteItem( 57 | title: "代理", 58 | icon: const Icon(Icons.cloud_outlined), 59 | path: "/proxys", 60 | module: ProxysModule(), 61 | ), 62 | MenuRouteItem( 63 | title: "日志", 64 | icon: const Icon(Icons.list_alt_outlined), 65 | path: "/logs", 66 | module: LogsModule(), 67 | ), 68 | MenuRouteItem( 69 | title: "连接", 70 | icon: const Icon(Icons.link_rounded), 71 | path: "/connections", 72 | module: ConnectionsModule(), 73 | ), 74 | MenuRouteItem( 75 | title: "订阅", 76 | icon: const Icon(Icons.code_rounded), 77 | path: "/profiles", 78 | module: ProfilesModule(), 79 | ), 80 | MenuRouteItem( 81 | title: "设置", 82 | icon: const Icon(Icons.settings_outlined), 83 | path: "/settings", 84 | module: SettingsModule(), 85 | ), 86 | ]); 87 | -------------------------------------------------------------------------------- /lib/app/pages/settings/settings_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/pages/settings/settings_page.dart'; 2 | import 'package:flutter_modular/flutter_modular.dart'; 3 | 4 | class SettingsModule extends Module { 5 | @override 6 | void routes(r) { 7 | r.child("/", child: (_) => const SettingsPage()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/source/app_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:clash_for_flutter/app/bean/clash_for_me_config_bean.dart'; 5 | import 'package:clash_for_flutter/app/bean/profile_base_bean.dart'; 6 | import 'package:clash_for_flutter/app/exceptions/message_exception.dart'; 7 | import 'package:clash_for_flutter/app/source/core_config.dart'; 8 | import 'package:clash_for_flutter/app/source/request.dart'; 9 | import 'package:clash_for_flutter/app/utils/constants.dart'; 10 | import 'package:flutter_modular/flutter_modular.dart'; 11 | import 'package:mobx/mobx.dart'; 12 | import 'package:path/path.dart' hide context; 13 | import 'package:proxy_manager/proxy_manager.dart'; 14 | 15 | part 'app_config.g.dart'; 16 | 17 | class AppConfig = AppConfigBase with _$AppConfig; 18 | 19 | final proxyManager = ProxyManager(); 20 | 21 | abstract class AppConfigBase with Store { 22 | final _request = Modular.get(); 23 | final _core = Modular.get(); 24 | 25 | final String profilesPath = "${Constants.homeDir.path}${Constants.profilesPath}"; 26 | 27 | @observable 28 | bool systemProxy = false; 29 | @observable 30 | ClashForMeConfig clashForMe = ClashForMeConfig.defaultConfig(); 31 | 32 | /// 当前应用中的配置文件 33 | @computed 34 | ProfileBase? get active { 35 | if (selectedFile == null) { 36 | return null; 37 | } 38 | return clashForMe.profiles.firstWhere((e) => e.file == selectedFile); 39 | } 40 | 41 | @computed 42 | String? get selectedFile => clashForMe.selectedFile; 43 | 44 | @computed 45 | bool get tunIf => clashForMe.tunIf ?? !Constants.isDesktop; 46 | 47 | @computed 48 | List get profiles => clashForMe.profiles; 49 | 50 | Future init() async { 51 | await _initConfig(); 52 | _initReaction(); 53 | } 54 | 55 | @action 56 | _initConfig() async { 57 | ClashForMeConfig? tempCfm = ClashForMeConfig.formFile(); 58 | tempCfm = await _profilesInitCheck(tempCfm); 59 | if (tempCfm != null) { 60 | clashForMe = tempCfm; 61 | } 62 | } 63 | 64 | _initReaction() { 65 | reaction( 66 | (_) => clashForMe, 67 | (ClashForMeConfig config) => config.saveFile(), 68 | delay: 1000, 69 | ); 70 | reaction( 71 | (_) => selectedFile, 72 | (String? file) { 73 | if (file == null) { 74 | return; 75 | } 76 | 77 | if (!File(file).isAbsolute) { 78 | file = "$profilesPath/$file"; 79 | } 80 | _request.changeConfig(file); 81 | }, 82 | ); 83 | } 84 | 85 | /// 校验本地订阅文件与配置里对应 86 | Future _profilesInitCheck(ClashForMeConfig? config) async { 87 | if (config == null) return null; 88 | 89 | var profilesDir = Directory(profilesPath); 90 | var fileList = []; 91 | if (profilesDir.existsSync()) { 92 | fileList = profilesDir.listSync().map((file) => basename(file.path)).toList(); 93 | } 94 | 95 | List profiles = config.profiles.where((e) => fileList.contains(e.file)).toList(); 96 | return config.copyWith(profiles: profiles); 97 | } 98 | 99 | @action 100 | setState({ 101 | String? selectedFile, 102 | List? profiles, 103 | String? mmdbUrl, 104 | String? delayTestUrl, 105 | bool? tunIf, 106 | }) { 107 | clashForMe = clashForMe.copyWith( 108 | selectedFile: selectedFile, 109 | profiles: profiles, 110 | mmdbUrl: mmdbUrl, 111 | delayTestUrl: delayTestUrl, 112 | tunIf: tunIf, 113 | ); 114 | } 115 | 116 | Future asyncProfile() { 117 | if (selectedFile == null) { 118 | return Future.value(true); 119 | } 120 | return _request.changeConfig("$profilesPath/$selectedFile"); 121 | } 122 | 123 | /// 打开代理 124 | @action 125 | Future openProxy() async { 126 | if (Constants.isDesktop) { 127 | int port = _core.mixedPort; 128 | if (port != 0) { 129 | if (!Platform.isWindows) { 130 | await proxyManager.setAsSystemProxy( 131 | ProxyTypes.socks, 132 | Constants.localhost, 133 | port, 134 | ); 135 | } else { 136 | await proxyManager.setAsSystemProxy( 137 | ProxyTypes.http, 138 | Constants.localhost, 139 | port, 140 | ); 141 | await proxyManager.setAsSystemProxy( 142 | ProxyTypes.https, 143 | Constants.localhost, 144 | port, 145 | ); 146 | } 147 | systemProxy = true; 148 | } else { 149 | throw MessageException("未设置代理端口"); 150 | } 151 | } 152 | } 153 | 154 | /// 关闭代理 155 | @action 156 | Future closeProxy() async { 157 | if (Constants.isDesktop) { 158 | proxyManager.cleanSystemProxy(); 159 | systemProxy = false; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /lib/app/source/app_config.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_config.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers 10 | 11 | mixin _$AppConfig on AppConfigBase, Store { 12 | Computed? _$activeComputed; 13 | 14 | @override 15 | ProfileBase? get active => 16 | (_$activeComputed ??= Computed(() => super.active, 17 | name: 'AppConfigBase.active')) 18 | .value; 19 | Computed? _$selectedFileComputed; 20 | 21 | @override 22 | String? get selectedFile => 23 | (_$selectedFileComputed ??= Computed(() => super.selectedFile, 24 | name: 'AppConfigBase.selectedFile')) 25 | .value; 26 | Computed? _$tunIfComputed; 27 | 28 | @override 29 | bool get tunIf => (_$tunIfComputed ??= 30 | Computed(() => super.tunIf, name: 'AppConfigBase.tunIf')) 31 | .value; 32 | Computed>? _$profilesComputed; 33 | 34 | @override 35 | List get profiles => 36 | (_$profilesComputed ??= Computed>(() => super.profiles, 37 | name: 'AppConfigBase.profiles')) 38 | .value; 39 | 40 | late final _$systemProxyAtom = 41 | Atom(name: 'AppConfigBase.systemProxy', context: context); 42 | 43 | @override 44 | bool get systemProxy { 45 | _$systemProxyAtom.reportRead(); 46 | return super.systemProxy; 47 | } 48 | 49 | @override 50 | set systemProxy(bool value) { 51 | _$systemProxyAtom.reportWrite(value, super.systemProxy, () { 52 | super.systemProxy = value; 53 | }); 54 | } 55 | 56 | late final _$clashForMeAtom = 57 | Atom(name: 'AppConfigBase.clashForMe', context: context); 58 | 59 | @override 60 | ClashForMeConfig get clashForMe { 61 | _$clashForMeAtom.reportRead(); 62 | return super.clashForMe; 63 | } 64 | 65 | @override 66 | set clashForMe(ClashForMeConfig value) { 67 | _$clashForMeAtom.reportWrite(value, super.clashForMe, () { 68 | super.clashForMe = value; 69 | }); 70 | } 71 | 72 | late final _$_initConfigAsyncAction = 73 | AsyncAction('AppConfigBase._initConfig', context: context); 74 | 75 | @override 76 | Future _initConfig() { 77 | return _$_initConfigAsyncAction.run(() => super._initConfig()); 78 | } 79 | 80 | late final _$openProxyAsyncAction = 81 | AsyncAction('AppConfigBase.openProxy', context: context); 82 | 83 | @override 84 | Future openProxy() { 85 | return _$openProxyAsyncAction.run(() => super.openProxy()); 86 | } 87 | 88 | late final _$closeProxyAsyncAction = 89 | AsyncAction('AppConfigBase.closeProxy', context: context); 90 | 91 | @override 92 | Future closeProxy() { 93 | return _$closeProxyAsyncAction.run(() => super.closeProxy()); 94 | } 95 | 96 | late final _$AppConfigBaseActionController = 97 | ActionController(name: 'AppConfigBase', context: context); 98 | 99 | @override 100 | dynamic setState( 101 | {String? selectedFile, 102 | List? profiles, 103 | String? mmdbUrl, 104 | String? delayTestUrl, 105 | bool? tunIf}) { 106 | final _$actionInfo = _$AppConfigBaseActionController.startAction( 107 | name: 'AppConfigBase.setState'); 108 | try { 109 | return super.setState( 110 | selectedFile: selectedFile, 111 | profiles: profiles, 112 | mmdbUrl: mmdbUrl, 113 | delayTestUrl: delayTestUrl, 114 | tunIf: tunIf); 115 | } finally { 116 | _$AppConfigBaseActionController.endAction(_$actionInfo); 117 | } 118 | } 119 | 120 | @override 121 | String toString() { 122 | return ''' 123 | systemProxy: ${systemProxy}, 124 | clashForMe: ${clashForMe}, 125 | active: ${active}, 126 | selectedFile: ${selectedFile}, 127 | tunIf: ${tunIf}, 128 | profiles: ${profiles} 129 | '''; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/app/source/core_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:clash_for_flutter/app/bean/config_bean.dart'; 4 | import 'package:clash_for_flutter/app/bean/tun_bean.dart'; 5 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 6 | import 'package:clash_for_flutter/app/source/request.dart'; 7 | import 'package:clash_for_flutter/app/utils/constants.dart'; 8 | import 'package:clash_for_flutter/core_control.dart'; 9 | import 'package:flutter_modular/flutter_modular.dart'; 10 | import 'package:mobx/mobx.dart'; 11 | 12 | part 'core_config.g.dart'; 13 | 14 | class CoreConfig = CoreConfigBase with _$CoreConfig; 15 | 16 | abstract class CoreConfigBase with Store { 17 | final _request = Modular.get(); 18 | 19 | @observable 20 | Config clash = Config.defaultConfig(); 21 | 22 | @computed 23 | bool get tunEnable => clash.tun?.enable ?? false; 24 | 25 | @computed 26 | int get mixedPort => clash.mixedPort ?? 0; 27 | 28 | void init() { 29 | reaction( 30 | (_) => clash, 31 | (Config config) { 32 | config.saveFile(); 33 | _request.patchConfigs(config); 34 | }, 35 | delay: 1000, 36 | ); 37 | } 38 | 39 | @action 40 | setState({ 41 | int? redirPort, 42 | int? tproxyPort, 43 | int? mixedPort, 44 | bool? allowLan, 45 | Mode? mode, 46 | LogLevel? logLevel, 47 | bool? ipv6, 48 | }) { 49 | clash = clash.copyWith( 50 | redirPort: redirPort, 51 | tproxyPort: tproxyPort, 52 | mixedPort: mixedPort, 53 | allowLan: allowLan, 54 | mode: mode, 55 | logLevel: logLevel, 56 | ipv6: ipv6, 57 | ); 58 | } 59 | 60 | @action 61 | Future asyncConfig() async { 62 | clash = await _request.getConfigs() ?? clash; 63 | } 64 | 65 | Future openTun() async { 66 | if (Constants.isDesktop) { 67 | await _request.patchConfigs(Config(tun: Tun(enable: true))); 68 | } else if (Platform.isAndroid) { 69 | await CoreControl.startVpn(); 70 | } 71 | await asyncConfig(); 72 | } 73 | 74 | Future closeTun() async { 75 | if (Constants.isDesktop) { 76 | await _request.patchConfigs(Config(tun: Tun(enable: false))); 77 | } else if (Platform.isAndroid) { 78 | await CoreControl.stopVpn(); 79 | } 80 | await asyncConfig(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/app/source/core_config.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'core_config.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers 10 | 11 | mixin _$CoreConfig on CoreConfigBase, Store { 12 | Computed? _$tunEnableComputed; 13 | 14 | @override 15 | bool get tunEnable => 16 | (_$tunEnableComputed ??= Computed(() => super.tunEnable, 17 | name: 'CoreConfigBase.tunEnable')) 18 | .value; 19 | Computed? _$mixedPortComputed; 20 | 21 | @override 22 | int get mixedPort => 23 | (_$mixedPortComputed ??= Computed(() => super.mixedPort, 24 | name: 'CoreConfigBase.mixedPort')) 25 | .value; 26 | 27 | late final _$clashAtom = Atom(name: 'CoreConfigBase.clash', context: context); 28 | 29 | @override 30 | Config get clash { 31 | _$clashAtom.reportRead(); 32 | return super.clash; 33 | } 34 | 35 | @override 36 | set clash(Config value) { 37 | _$clashAtom.reportWrite(value, super.clash, () { 38 | super.clash = value; 39 | }); 40 | } 41 | 42 | late final _$asyncConfigAsyncAction = 43 | AsyncAction('CoreConfigBase.asyncConfig', context: context); 44 | 45 | @override 46 | Future asyncConfig() { 47 | return _$asyncConfigAsyncAction.run(() => super.asyncConfig()); 48 | } 49 | 50 | late final _$CoreConfigBaseActionController = 51 | ActionController(name: 'CoreConfigBase', context: context); 52 | 53 | @override 54 | dynamic setState( 55 | {int? redirPort, 56 | int? tproxyPort, 57 | int? mixedPort, 58 | bool? allowLan, 59 | Mode? mode, 60 | LogLevel? logLevel, 61 | bool? ipv6}) { 62 | final _$actionInfo = _$CoreConfigBaseActionController.startAction( 63 | name: 'CoreConfigBase.setState'); 64 | try { 65 | return super.setState( 66 | redirPort: redirPort, 67 | tproxyPort: tproxyPort, 68 | mixedPort: mixedPort, 69 | allowLan: allowLan, 70 | mode: mode, 71 | logLevel: logLevel, 72 | ipv6: ipv6); 73 | } finally { 74 | _$CoreConfigBaseActionController.endAction(_$actionInfo); 75 | } 76 | } 77 | 78 | @override 79 | String toString() { 80 | return ''' 81 | clash: ${clash}, 82 | tunEnable: ${tunEnable}, 83 | mixedPort: ${mixedPort} 84 | '''; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/app/source/logs_subscription.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection'; 3 | 4 | import 'package:clash_for_flutter/app/bean/log_bean.dart'; 5 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 6 | import 'package:clash_for_flutter/app/source/core_config.dart'; 7 | import 'package:clash_for_flutter/app/source/request.dart'; 8 | import 'package:clash_for_flutter/app/utils/constants.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_modular/flutter_modular.dart'; 11 | import 'package:mobx/mobx.dart'; 12 | 13 | /// 日志订阅 14 | class LogsSubscription extends ChangeNotifier implements Disposable { 15 | final _core = Modular.get(); 16 | final _request = Modular.get(); 17 | final Queue _logQueue = Queue(); 18 | StreamSubscription? _subscription; 19 | 20 | List get logList => _logQueue.toList(); 21 | 22 | @override 23 | void dispose() { 24 | _subscription?.cancel(); 25 | super.dispose(); 26 | } 27 | 28 | void startSubLogs() { 29 | reaction((_) => _core.clash.logLevel ?? LogLevel.info, (level) { 30 | _subscription?.cancel(); 31 | _subscription = _request.logs(level).listen((event) { 32 | if (event == null) { 33 | return; 34 | } 35 | 36 | if (_logQueue.length >= Constants.logsCapacity) { 37 | _logQueue.removeFirst(); 38 | } 39 | _logQueue.add(event); 40 | notifyListeners(); 41 | }); 42 | }, fireImmediately: true); 43 | } 44 | 45 | void clearLogs() { 46 | _logQueue.clear(); 47 | notifyListeners(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/app/utils/clash_custom_messages.dart: -------------------------------------------------------------------------------- 1 | import 'package:timeago/timeago.dart'; 2 | 3 | class ClashCustomMessages implements LookupMessages { 4 | @override 5 | String prefixAgo() => ''; 6 | 7 | @override 8 | String prefixFromNow() => ''; 9 | 10 | @override 11 | String suffixAgo() => '前'; 12 | 13 | @override 14 | String suffixFromNow() => '后'; 15 | 16 | @override 17 | String lessThanOneMinute(int seconds) => '几秒'; 18 | 19 | @override 20 | String aboutAMinute(int minutes) => '约一分钟'; 21 | 22 | @override 23 | String minutes(int minutes) => '$minutes 分钟'; 24 | 25 | @override 26 | String aboutAnHour(int minutes) => '约一小时'; 27 | 28 | @override 29 | String hours(int hours) => '$hours 小时'; 30 | 31 | @override 32 | String aDay(int hours) => '约一天'; 33 | 34 | @override 35 | String days(int days) => '$days 天'; 36 | 37 | @override 38 | String aboutAMonth(int days) => '约一月'; 39 | 40 | @override 41 | String months(int months) => '$months 月'; 42 | 43 | @override 44 | String aboutAYear(int year) => '约一年'; 45 | 46 | @override 47 | String years(int years) => '$years 年'; 48 | 49 | @override 50 | String wordSeparator() => ''; 51 | } 52 | -------------------------------------------------------------------------------- /lib/app/utils/constants.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | /// 常量类 4 | class Constants { 5 | static final isDesktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux; 6 | 7 | /// rust 控制服务地址 8 | static late final String rustAddr; 9 | 10 | /// 配置目录 11 | static late final Directory homeDir; 12 | 13 | /// 开源地址 14 | static const sourceUrl = "https://github.com/mapleafgo/clash-for-flutter"; 15 | 16 | /// 官网 17 | static const homeUrl = "https://mapleafgo.github.io/clash-for-flutter"; 18 | 19 | /// 检测最新版本 20 | static const releaseUrl = "https://api.github.com/repos/mapleafgo/clash-for-flutter/releases/latest"; 21 | 22 | /// 下载的配置文件路径 23 | static const profilesPath = "/profiles"; 24 | 25 | /// clash的配置 26 | static const clashConfig = "/config.yaml"; 27 | 28 | /// 该程序的一些配置 29 | static const clashForMe = "/cfm.json"; 30 | 31 | /// mmdb 保存路径 32 | static const mmdb = "/Country.mmdb"; 33 | 34 | /// mmdb 更新保存路径 35 | static const mmdb_new = "/Country_new.mmdb"; 36 | 37 | /// localhost 38 | static const localhost = "127.0.0.1"; 39 | 40 | /// 日志最大容量 41 | static const logsCapacity = 1000; 42 | } 43 | 44 | /// 默认配置值 45 | class DefaultConfigValue { 46 | static const mmdbUrl = "http://www.ideame.top/mmdb/Country.mmdb"; 47 | 48 | static const delayTestUrl = "http://www.gstatic.com/generate_204"; 49 | } 50 | -------------------------------------------------------------------------------- /lib/app/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:clash_for_flutter/app/enum/type_enum.dart'; 2 | 3 | String dataformat(int value) { 4 | double num = value.toDouble(); 5 | var level = 0; 6 | for (; num.compareTo(1024) > 0; level++) { 7 | num /= 1024; 8 | } 9 | return "${num.toStringAsFixed(1)} ${DataUnit.values[level].value}"; 10 | } 11 | -------------------------------------------------------------------------------- /lib/core_control.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ffi'; 2 | import 'dart:io'; 3 | 4 | import 'package:clash_for_flutter/app/utils/constants.dart'; 5 | import 'package:clash_for_flutter/clash_generated_bindings.dart'; 6 | import 'package:ffi/ffi.dart'; 7 | import 'package:flutter/services.dart'; 8 | 9 | class CoreControl { 10 | static const MethodChannel _channel = MethodChannel('cn.mapleafgo/socks_vpn_plugin'); 11 | static late final Clash _clash; 12 | 13 | // 初始化clash 14 | static void init() { 15 | if (!Constants.isDesktop) { 16 | return; 17 | } 18 | 19 | String fullPath = ""; 20 | if (Platform.isWindows) { 21 | fullPath = "libclash.dll"; 22 | } else if (Platform.isMacOS) { 23 | fullPath = "libclash.dylib"; 24 | } else { 25 | fullPath = "libclash.so"; 26 | } 27 | final lib = DynamicLibrary.open(fullPath); 28 | _clash = Clash(lib); 29 | } 30 | 31 | static Future startVpn() { 32 | return _channel.invokeMethod('startVpn'); 33 | } 34 | 35 | static Future stopVpn() { 36 | return _channel.invokeMethod('stopVpn'); 37 | } 38 | 39 | static Future startService() { 40 | if (Constants.isDesktop) { 41 | return Future.sync(() => _clash.StartService() == 1); 42 | } 43 | return _channel.invokeMethod('startService'); 44 | } 45 | 46 | static Future setConfig(File config) { 47 | if (Constants.isDesktop) { 48 | return Future.sync(() => _clash.SetConfig(config.path.toNativeUtf8().cast()) == 1); 49 | } 50 | return _channel.invokeMethod('setConfig', {"config": config.path}); 51 | } 52 | 53 | static Future setHomeDir(Directory dir) { 54 | if (Constants.isDesktop) { 55 | return Future.sync(() => _clash.SetHomeDir(dir.path.toNativeUtf8().cast()) == 1); 56 | } 57 | return _channel.invokeMethod('setHomeDir', {"dir": dir.path}); 58 | } 59 | 60 | static Future startRust(String addr) { 61 | if (Constants.isDesktop) { 62 | return Future.sync(() => _clash.StartRust(addr.toNativeUtf8().cast()).cast().toDartString()); 63 | } 64 | return _channel.invokeMethod('startRust', {"addr": addr}); 65 | } 66 | 67 | static Future verifyMMDB(String path) { 68 | if (Constants.isDesktop) { 69 | return Future.sync(() => _clash.VerifyMMDB(path.toNativeUtf8().cast()) == 1); 70 | } 71 | return _channel.invokeMethod('verifyMMDB', {"path": path}); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:math'; 3 | 4 | import 'package:clash_for_flutter/app/app_module.dart'; 5 | import 'package:clash_for_flutter/app/app_widget.dart'; 6 | import 'package:clash_for_flutter/app/utils/clash_custom_messages.dart'; 7 | import 'package:clash_for_flutter/core_control.dart'; 8 | import 'package:dart_json_mapper/dart_json_mapper.dart'; 9 | import 'package:dart_json_mapper_mobx/dart_json_mapper_mobx.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_modular/flutter_modular.dart'; 12 | import 'package:path_provider/path_provider.dart'; 13 | import 'package:protocol_handler/protocol_handler.dart'; 14 | import 'package:timeago/timeago.dart' as timeago; 15 | import 'package:window_manager/window_manager.dart'; 16 | import 'package:clash_for_flutter/app/bean/config_bean.dart'; 17 | 18 | import 'app/utils/constants.dart'; 19 | import 'main.mapper.g.dart' show initializeJsonMapper; 20 | 21 | void main() async { 22 | WidgetsFlutterBinding.ensureInitialized(); 23 | 24 | initializeJsonMapper(adapters: [mobXAdapter]); 25 | JsonMapper().useAdapter(JsonMapperAdapter(valueDecorators: { 26 | typeOf>(): (value) { 27 | return Map.castFrom(value); 28 | }, 29 | })); 30 | 31 | if (Constants.isDesktop) { 32 | await windowManager.ensureInitialized(); 33 | if (!Platform.isLinux) { 34 | await protocolHandler.register('clash'); 35 | } 36 | 37 | WindowOptions windowOptions = const WindowOptions( 38 | minimumSize: Size(460, 600), 39 | size: Size(900, 650), 40 | center: true, 41 | backgroundColor: Colors.transparent, 42 | skipTaskbar: false, 43 | titleBarStyle: TitleBarStyle.hidden, 44 | ); 45 | 46 | await windowManager.waitUntilReadyToShow(windowOptions, () async { 47 | await windowManager.show(); 48 | await windowManager.focus(); 49 | }); 50 | } 51 | 52 | timeago.setLocaleMessages('zh_cn', ClashCustomMessages()); 53 | 54 | // 初始化 Clash 55 | CoreControl.init(); 56 | await getApplicationSupportDirectory().then((dir) => Constants.homeDir = dir); 57 | // 设置主目录 58 | await CoreControl.setHomeDir(Constants.homeDir); 59 | // 创建默认配置文件 60 | if (!(Config.fileExist() ?? false)) { 61 | await Config.defaultConfig().saveFile(); 62 | } 63 | // 启动 rust 控制服务,端口随机 64 | await CoreControl.startRust("${Constants.localhost}:${Random().nextInt(9999) + 10000}") 65 | .then((addr) => Constants.rustAddr = addr ?? ""); 66 | // 启动内核 67 | await CoreControl.startService(); 68 | 69 | runApp(ModularApp( 70 | module: AppModule(), 71 | child: const AppWidget(), 72 | )); 73 | } 74 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/core/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/linux/core/.gitkeep -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | void fl_register_plugins(FlPluginRegistry* registry) { 18 | g_autoptr(FlPluginRegistrar) desktop_lifecycle_registrar = 19 | fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopLifecyclePlugin"); 20 | desktop_lifecycle_plugin_register_with_registrar(desktop_lifecycle_registrar); 21 | g_autoptr(FlPluginRegistrar) local_notifier_registrar = 22 | fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin"); 23 | local_notifier_plugin_register_with_registrar(local_notifier_registrar); 24 | g_autoptr(FlPluginRegistrar) proxy_manager_registrar = 25 | fl_plugin_registry_get_registrar_for_plugin(registry, "ProxyManagerPlugin"); 26 | proxy_manager_plugin_register_with_registrar(proxy_manager_registrar); 27 | g_autoptr(FlPluginRegistrar) screen_retriever_registrar = 28 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); 29 | screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); 30 | g_autoptr(FlPluginRegistrar) tray_manager_registrar = 31 | fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); 32 | tray_manager_plugin_register_with_registrar(tray_manager_registrar); 33 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 34 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 35 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 36 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 37 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 38 | window_manager_plugin_register_with_registrar(window_manager_registrar); 39 | } 40 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | desktop_lifecycle 7 | local_notifier 8 | proxy_manager 9 | screen_retriever 10 | tray_manager 11 | url_launcher_linux 12 | window_manager 13 | ) 14 | 15 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 16 | ) 17 | 18 | set(PLUGIN_BUNDLED_LIBRARIES) 19 | 20 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 22 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 25 | endforeach(plugin) 26 | 27 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 28 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 29 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 30 | endforeach(ffi_plugin) 31 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen* screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "clash_for_flutter"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } else { 47 | gtk_window_set_title(window, "clash_for_flutter"); 48 | } 49 | 50 | gtk_window_set_default_size(window, 1280, 720); 51 | gtk_widget_realize(GTK_WIDGET(window)); 52 | 53 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 55 | 56 | FlView* view = fl_view_new(project); 57 | gtk_widget_show(GTK_WIDGET(view)); 58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 59 | 60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 61 | 62 | gtk_widget_grab_focus(GTK_WIDGET(view)); 63 | } 64 | 65 | // Implements GApplication::local_command_line. 66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 67 | MyApplication* self = MY_APPLICATION(application); 68 | // Strip out the first argument as it is the binary name. 69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 70 | 71 | g_autoptr(GError) error = nullptr; 72 | if (!g_application_register(application, nullptr, &error)) { 73 | g_warning("Failed to register: %s", error->message); 74 | *exit_status = 1; 75 | return TRUE; 76 | } 77 | 78 | g_application_activate(application); 79 | *exit_status = 0; 80 | 81 | return TRUE; 82 | } 83 | 84 | // Implements GObject::dispose. 85 | static void my_application_dispose(GObject* object) { 86 | MyApplication* self = MY_APPLICATION(object); 87 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 88 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 89 | } 90 | 91 | static void my_application_class_init(MyApplicationClass* klass) { 92 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 93 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 94 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 95 | } 96 | 97 | static void my_application_init(MyApplication* self) {} 98 | 99 | MyApplication* my_application_new() { 100 | return MY_APPLICATION(g_object_new(my_application_get_type(), 101 | "application-id", APPLICATION_ID, 102 | "flags", G_APPLICATION_NON_UNIQUE, 103 | nullptr)); 104 | } 105 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /linux/packaging/appimage/make_config.yaml: -------------------------------------------------------------------------------- 1 | display_name: Clash for Flutter 2 | icon: assets/icon.png 3 | generic_name: Clash Application 4 | categories: 5 | - Network 6 | - Science 7 | - Development 8 | - System 9 | keywords: 10 | - Clash 11 | - Flutter 12 | - Application 13 | startup_notify: true 14 | include: [] 15 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import desktop_lifecycle 9 | import local_notifier 10 | import package_info_plus 11 | import path_provider_foundation 12 | import protocol_handler 13 | import proxy_manager 14 | import screen_retriever 15 | import tray_manager 16 | import url_launcher_macos 17 | import window_manager 18 | 19 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 20 | DesktopLifecyclePlugin.register(with: registry.registrar(forPlugin: "DesktopLifecyclePlugin")) 21 | LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) 22 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 23 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 24 | ProtocolHandlerPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerPlugin")) 25 | ProxyManagerPlugin.register(with: registry.registrar(forPlugin: "ProxyManagerPlugin")) 26 | ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) 27 | TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) 28 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 29 | WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) 30 | } 31 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.11' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | end 35 | 36 | post_install do |installer| 37 | installer.pods_project.targets.each do |target| 38 | flutter_additional_macos_build_settings(target) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | //TODO: 关闭时退出 8 | return false 9 | } 10 | 11 | override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { 12 | if !flag { 13 | for window in NSApp.windows { 14 | if !window.isVisible { 15 | window.setIsVisible(true) 16 | } 17 | window.makeKeyAndOrderFront(self) 18 | NSApp.activate(ignoringOtherApps: true) 19 | } 20 | } 21 | return true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = clash_for_flutter 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = cn.mapleafgo.clashForFlutter 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2022 cn.mapleafgo. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | CFBundleURLTypes 30 | 31 | 32 | CFBundleTypeRole 33 | Editor 34 | CFBundleURLName 35 | 36 | CFBundleURLSchemes 37 | 38 | clash 39 | 40 | 41 | 42 | NSPrincipalClass 43 | NSApplication 44 | 45 | 46 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import window_manager 4 | 5 | class MainFlutterWindow: NSWindow { 6 | override func awakeFromNib() { 7 | let flutterViewController = FlutterViewController.init() 8 | let windowFrame = self.frame 9 | self.contentViewController = flutterViewController 10 | self.setFrame(windowFrame, display: true) 11 | 12 | RegisterGeneratedPlugins(registry: flutterViewController) 13 | 14 | super.awakeFromNib() 15 | } 16 | 17 | override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { 18 | super.order(place, relativeTo: otherWin) 19 | hiddenWindowAtLaunch() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /macos/packaging/dmg/make_config.yaml: -------------------------------------------------------------------------------- 1 | title: Clash for Flutter 2 | contents: 3 | - x: 448 4 | y: 344 5 | type: link 6 | path: "/Applications" 7 | - x: 192 8 | y: 344 9 | type: file 10 | path: clash_for_flutter.app 11 | -------------------------------------------------------------------------------- /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 in the flutter_test package. 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:clash_for_flutter/app/app_widget.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(const AppWidget()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(clash_for_flutter LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "clash_for_flutter") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(SET CMP0063 NEW) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | # Generated plugin build rules, which manage building the plugins and adding 56 | # them to the application. 57 | include(flutter/generated_plugins.cmake) 58 | 59 | 60 | # === Installation === 61 | # Support files are copied into place next to the executable, so that it can 62 | # run in place. This is done instead of making a separate bundle (as on Linux) 63 | # so that building and running from within Visual Studio will work. 64 | set(BUILD_BUNDLE_DIR "$") 65 | # Make the "install" step default, as it's required to run. 66 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 67 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 68 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 69 | endif() 70 | 71 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 72 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 73 | 74 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 75 | COMPONENT Runtime) 76 | 77 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 78 | COMPONENT Runtime) 79 | 80 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 81 | COMPONENT Runtime) 82 | 83 | install(FILES "core/libclash.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 84 | COMPONENT Runtime) 85 | 86 | if(PLUGIN_BUNDLED_LIBRARIES) 87 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 88 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 89 | COMPONENT Runtime) 90 | endif() 91 | 92 | # Fully re-copy the assets directory on each build to avoid having stale files 93 | # from a previous install. 94 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 95 | install(CODE " 96 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 97 | " COMPONENT Runtime) 98 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 99 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 100 | 101 | # Install the AOT library on non-Debug builds only. 102 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 103 | CONFIGURATIONS Profile;Release 104 | COMPONENT Runtime) 105 | -------------------------------------------------------------------------------- /windows/core/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/windows/core/.gitkeep -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | void RegisterPlugins(flutter::PluginRegistry* registry) { 19 | DesktopLifecyclePluginRegisterWithRegistrar( 20 | registry->GetRegistrarForPlugin("DesktopLifecyclePlugin")); 21 | LocalNotifierPluginRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("LocalNotifierPlugin")); 23 | ProtocolHandlerPluginRegisterWithRegistrar( 24 | registry->GetRegistrarForPlugin("ProtocolHandlerPlugin")); 25 | ProxyManagerPluginRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("ProxyManagerPlugin")); 27 | ScreenRetrieverPluginRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); 29 | TrayManagerPluginRegisterWithRegistrar( 30 | registry->GetRegistrarForPlugin("TrayManagerPlugin")); 31 | UrlLauncherWindowsRegisterWithRegistrar( 32 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 33 | WindowManagerPluginRegisterWithRegistrar( 34 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 35 | } 36 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | desktop_lifecycle 7 | local_notifier 8 | protocol_handler 9 | proxy_manager 10 | screen_retriever 11 | tray_manager 12 | url_launcher_windows 13 | window_manager 14 | ) 15 | 16 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 17 | ) 18 | 19 | set(PLUGIN_BUNDLED_LIBRARIES) 20 | 21 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 23 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 26 | endforeach(plugin) 27 | 28 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 29 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 30 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 31 | endforeach(ffi_plugin) 32 | -------------------------------------------------------------------------------- /windows/packaging/exe/make_config.yaml: -------------------------------------------------------------------------------- 1 | app_id: B3039CF4-E394-42B6-971C-4BE97F322DE8 2 | publisher: mapleafgo 3 | publisher_url: https://github.com/mapleafgo/clash-for-flutter 4 | display_name: Clash for Flutter 5 | create_desktop_icon: true 6 | install_dir_name: ClashForFlutter 7 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 37 | 38 | # Run the Flutter tool portions of the build. This must not be removed. 39 | add_dependencies(${BINARY_NAME} flutter_assemble) 40 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "cn.mapleafgo" "\0" 93 | VALUE "FileDescription", "clash_for_flutter" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "clash_for_flutter" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 cn.mapleafgo. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "clash_for_flutter.exe" "\0" 98 | VALUE "ProductName", "clash_for_flutter" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // TODO: 单例应用 11 | HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"clash_for_flutter"); 12 | if (hwnd != NULL) { 13 | ::ShowWindow(hwnd, SW_NORMAL); 14 | ::SetForegroundWindow(hwnd); 15 | return EXIT_FAILURE; 16 | } 17 | 18 | // Attach to console when present (e.g., 'flutter run') or create a 19 | // new console when running with a debugger. 20 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 21 | CreateAndAttachConsole(); 22 | } 23 | 24 | // Initialize COM, so that it is available for use in the library and/or 25 | // plugins. 26 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 27 | 28 | flutter::DartProject project(L"data"); 29 | 30 | std::vector command_line_arguments = 31 | GetCommandLineArguments(); 32 | 33 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 34 | 35 | FlutterWindow window(project); 36 | Win32Window::Point origin(10, 10); 37 | Win32Window::Size size(1280, 720); 38 | if (!window.CreateAndShow(L"clash_for_flutter", origin, size)) { 39 | return EXIT_FAILURE; 40 | } 41 | window.SetQuitOnClose(true); 42 | 43 | ::MSG msg; 44 | while (::GetMessage(&msg, nullptr, 0, 0)) { 45 | ::TranslateMessage(&msg); 46 | ::DispatchMessage(&msg); 47 | } 48 | 49 | ::CoUninitialize(); 50 | return EXIT_SUCCESS; 51 | } 52 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapleafgo/clash-for-flutter/3313fa4936202219af63f5fb04b299c33fc86d25/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | std::string utf8_string; 52 | if (target_length == 0 || target_length > utf8_string.max_size()) { 53 | return utf8_string; 54 | } 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | --------------------------------------------------------------------------------