├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── .metadata ├── LICENSE ├── README.md ├── README_zh_CN.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-playstore.png │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── follow │ │ │ │ └── clash │ │ │ │ ├── BaseServiceInterface.kt │ │ │ │ ├── FilesProvider.kt │ │ │ │ ├── GlobalState.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── TempActivity.kt │ │ │ │ ├── extensions │ │ │ │ └── Ext.kt │ │ │ │ ├── models │ │ │ │ ├── Package.kt │ │ │ │ ├── Process.kt │ │ │ │ └── Props.kt │ │ │ │ ├── plugins │ │ │ │ ├── AppPlugin.kt │ │ │ │ ├── ServicePlugin.kt │ │ │ │ ├── TilePlugin.kt │ │ │ │ └── VpnPlugin.kt │ │ │ │ └── services │ │ │ │ ├── FlClashService.kt │ │ │ │ ├── FlClashTileService.kt │ │ │ │ └── FlClashVpnService.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── ic_stat_name.png │ │ │ ├── drawable-mdpi │ │ │ └── ic_stat_name.png │ │ │ ├── drawable-xhdpi │ │ │ └── ic_stat_name.png │ │ │ ├── drawable-xxhdpi │ │ │ └── ic_stat_name.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── ic_stat_name.png │ │ │ ├── drawable │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── values-night-v27 │ │ │ └── styles.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values-v27 │ │ │ └── styles.xml │ │ │ ├── values │ │ │ ├── ic_launcher_background.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ ├── file_paths.xml │ │ │ └── network_security_config.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── data │ ├── ASN.mmdb │ ├── GeoIP.dat │ ├── GeoSite.dat │ └── geoip.metadb ├── fonts │ ├── Icons.ttf │ └── Twemoji.Mozilla.ttf └── images │ ├── avatars │ ├── arue.jpg │ └── june2.jpg │ ├── icon.ico │ ├── icon.png │ ├── icon_black.ico │ ├── icon_black.png │ ├── icon_white.ico │ └── icon_white.png ├── build.yaml ├── core ├── common.go ├── dart-bridge │ ├── include │ │ ├── dart_api.h │ │ ├── dart_api_dl.c │ │ ├── dart_api_dl.h │ │ ├── dart_native_api.h │ │ ├── dart_tools_api.h │ │ ├── dart_version.h │ │ └── internal │ │ │ └── dart_api_dl_impl.h │ └── lib.go ├── dns.go ├── go.mod ├── go.sum ├── hub.go ├── log.go ├── main.go ├── message.go ├── platform │ └── limit.go ├── process.go ├── state.go ├── state │ └── state.go ├── tun.go └── tun │ └── tun.go ├── distribute_options.yaml ├── lib ├── application.dart ├── clash │ ├── clash.dart │ ├── core.dart │ ├── generated │ │ └── clash_ffi.dart │ ├── message.dart │ └── service.dart ├── common │ ├── android.dart │ ├── app_localizations.dart │ ├── archive.dart │ ├── color.dart │ ├── common.dart │ ├── constant.dart │ ├── context.dart │ ├── datetime.dart │ ├── dav_client.dart │ ├── function.dart │ ├── http.dart │ ├── icons.dart │ ├── iterable.dart │ ├── keyboard.dart │ ├── launch.dart │ ├── link.dart │ ├── list.dart │ ├── measure.dart │ ├── navigation.dart │ ├── network.dart │ ├── num.dart │ ├── other.dart │ ├── package.dart │ ├── path.dart │ ├── picker.dart │ ├── preferences.dart │ ├── protocol.dart │ ├── proxy.dart │ ├── request.dart │ ├── scroll.dart │ ├── string.dart │ ├── system.dart │ ├── text.dart │ ├── window.dart │ └── windows.dart ├── controller.dart ├── enum │ └── enum.dart ├── fragments │ ├── about.dart │ ├── access.dart │ ├── application_setting.dart │ ├── backup_and_recovery.dart │ ├── config │ │ ├── config.dart │ │ ├── dns.dart │ │ ├── general.dart │ │ └── network.dart │ ├── connections.dart │ ├── dashboard │ │ ├── core_info.dart │ │ ├── dashboard.dart │ │ ├── intranet_ip.dart │ │ ├── network_detection.dart │ │ ├── network_speed.dart │ │ ├── outbound_mode.dart │ │ ├── start_button.dart │ │ ├── status_switch.dart │ │ └── traffic_usage.dart │ ├── fragments.dart │ ├── hotkey.dart │ ├── logs.dart │ ├── profiles │ │ ├── add_profile.dart │ │ ├── edit_profile.dart │ │ ├── profiles.dart │ │ └── view_profile.dart │ ├── proxies │ │ ├── card.dart │ │ ├── common.dart │ │ ├── list.dart │ │ ├── providers.dart │ │ ├── proxies.dart │ │ ├── setting.dart │ │ └── tab.dart │ ├── requests.dart │ ├── resources.dart │ ├── theme.dart │ └── tools.dart ├── l10n │ ├── arb │ │ ├── intl_en.arb │ │ └── intl_zh_CN.arb │ ├── intl │ │ ├── messages_all.dart │ │ ├── messages_en.dart │ │ └── messages_zh_CN.dart │ └── l10n.dart ├── main.dart ├── manager │ ├── android_manager.dart │ ├── app_state_manager.dart │ ├── clash_manager.dart │ ├── hotkey_manager.dart │ ├── manager.dart │ ├── media_manager.dart │ ├── proxy_manager.dart │ ├── tile_manager.dart │ ├── tray_manager.dart │ ├── vpn_manager.dart │ └── window_manager.dart ├── models │ ├── app.dart │ ├── clash_config.dart │ ├── common.dart │ ├── config.dart │ ├── ffi.dart │ ├── generated │ │ ├── clash_config.freezed.dart │ │ ├── clash_config.g.dart │ │ ├── common.freezed.dart │ │ ├── common.g.dart │ │ ├── config.freezed.dart │ │ ├── config.g.dart │ │ ├── ffi.freezed.dart │ │ ├── ffi.g.dart │ │ ├── profile.freezed.dart │ │ ├── profile.g.dart │ │ └── selector.freezed.dart │ ├── models.dart │ ├── profile.dart │ └── selector.dart ├── pages │ ├── home.dart │ ├── pages.dart │ └── scan.dart ├── plugins │ ├── app.dart │ ├── service.dart │ ├── tile.dart │ └── vpn.dart ├── router │ └── fade_page.dart ├── state.dart └── widgets │ ├── animate_grid.dart │ ├── back_scope.dart │ ├── builder.dart │ ├── card.dart │ ├── chip.dart │ ├── color_scheme_box.dart │ ├── connection_item.dart │ ├── disabled_mask.dart │ ├── fade_box.dart │ ├── float_layout.dart │ ├── grid.dart │ ├── icon.dart │ ├── input.dart │ ├── keep_scope.dart │ ├── line_chart.dart │ ├── list.dart │ ├── null_status.dart │ ├── open_container.dart │ ├── popup_menu.dart │ ├── scaffold.dart │ ├── setting.dart │ ├── sheet.dart │ ├── side_sheet.dart │ ├── text.dart │ └── widgets.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── 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 │ ├── deb │ └── make_config.yaml │ └── rpm │ └── make_config.yaml ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── 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 │ │ └── Contents.json │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements ├── RunnerTests │ └── RunnerTests.swift └── packaging │ └── dmg │ └── make_config.yaml ├── plugins └── proxy │ ├── .gitignore │ ├── .metadata │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── lib │ ├── proxy.dart │ ├── proxy_method_channel.dart │ └── proxy_platform_interface.dart │ ├── pubspec.yaml │ └── windows │ ├── .gitignore │ ├── .vscode │ └── settings.json │ ├── CMakeLists.txt │ ├── include │ └── proxy │ │ └── proxy_plugin_c_api.h │ ├── proxy_plugin.cpp │ ├── proxy_plugin.h │ ├── proxy_plugin_c_api.cpp │ └── test │ └── proxy_plugin_test.cpp ├── pubspec.lock ├── pubspec.yaml ├── setup.dart ├── snapshots ├── desktop.gif ├── get-it-on-fdroid.svg ├── get-it-on-github.svg └── mobile.gif ├── test └── command_test.dart └── windows ├── .gitignore ├── CMakeLists.txt ├── EnableLoopback.exe ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake ├── packaging └── exe │ ├── ChineseSimplified.isl │ └── 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 /.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 | /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 | 48 | 49 | #libclash 50 | /libclash/ 51 | 52 | #jniLibs 53 | /android/app/src/main/jniLibs/ 54 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "core/Clash.Meta"] 2 | path = core/Clash.Meta 3 | url = git@github.com:chen08209/Clash.Meta.git 4 | branch = FlClash-Alpha 5 | [submodule "plugins/flutter_distributor"] 6 | path = plugins/flutter_distributor 7 | url = git@github.com:chen08209/flutter_distributor.git 8 | branch = FlClash 9 | -------------------------------------------------------------------------------- /.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: 796c8ef79279f9c774545b3771238c3098dbefab 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: 796c8ef79279f9c774545b3771238c3098dbefab 17 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 18 | - platform: android 19 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 20 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 21 | - platform: ios 22 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 23 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 24 | - platform: linux 25 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 26 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 27 | - platform: macos 28 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 29 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 30 | - platform: web 31 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 32 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 33 | - platform: windows 34 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 35 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /README_zh_CN.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [**English**](README.md) 4 | 5 |
6 | 7 | ## FlClash 8 | 9 | [![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE) 10 | 11 | [![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash) 12 | 13 | 14 | 基于ClashMeta的多平台代理客户端,简单易用,开源无广告。 15 | 16 | on Desktop: 17 |

18 | desktop 19 |

20 | 21 | on Mobile: 22 |

23 | mobile 24 |

25 | 26 | ## Features 27 | 28 | ✈️ 多平台: Android, Windows, macOS and Linux 29 | 30 | 💻 自适应多个屏幕尺寸,多种颜色主题可供选择 31 | 32 | 💡 基本 Material You 设计, 类[Surfboard](https://github.com/getsurfboard/surfboard)用户界面 33 | 34 | ☁️ 支持通过WebDAV同步数据 35 | 36 | ✨ 支持一键导入订阅, 深色模式 37 | 38 | ## Download 39 | 40 | Get it on F-Droid Get it on GitHub 41 | 42 | ## Build 43 | 44 | 1. 更新 submodules 45 | ```bash 46 | git submodule update --init --recursive 47 | ``` 48 | 49 | 2. 安装 `Flutter` 以及 `Golang` 环境 50 | 51 | 3. 构建应用 52 | 53 | - android 54 | 55 | 1. 安装 `Android SDK` , `Android NDK` 56 | 57 | 2. 设置 `ANDROID_NDK` 环境变量 58 | 59 | 3. 运行构建脚本 60 | 61 | ```bash 62 | dart .\setup.dart android 63 | ``` 64 | 65 | - windows 66 | 67 | 1. 你需要一个windows客户端 68 | 69 | 2. 安装 `Gcc`,`Inno Setup` 70 | 71 | 3. 运行构建脚本 72 | 73 | ```bash 74 | dart .\setup.dart 75 | ``` 76 | 77 | - linux 78 | 79 | 1. 你需要一个linux客户端 80 | 81 | 2. 运行构建脚本 82 | 83 | ```bash 84 | dart .\setup.dart 85 | ``` 86 | 87 | - macOS 88 | 89 | 1. 你需要一个macOS客户端 90 | 91 | 2. 运行构建脚本 92 | 93 | ```bash 94 | dart .\setup.dart 95 | ``` 96 | 97 | ## Star History 98 | 99 | 支持开发者的最简单方式是点击页面顶部的星标(⭐)。 100 | 101 |

102 | 103 | start 104 | 105 |

-------------------------------------------------------------------------------- /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/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | 2 | -keep class com.follow.clash.models.**{ *; } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/follow/clash/BaseServiceInterface.kt: -------------------------------------------------------------------------------- 1 | package com.follow.clash 2 | 3 | 4 | import com.follow.clash.models.VpnOptions 5 | 6 | interface BaseServiceInterface { 7 | fun start(options: VpnOptions): Int 8 | fun stop() 9 | fun startForeground(title: String, content: String) 10 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/follow/clash/GlobalState.kt: -------------------------------------------------------------------------------- 1 | package com.follow.clash 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.MutableLiveData 5 | import com.follow.clash.plugins.AppPlugin 6 | import com.follow.clash.plugins.ServicePlugin 7 | import com.follow.clash.plugins.VpnPlugin 8 | import com.follow.clash.plugins.TilePlugin 9 | import io.flutter.FlutterInjector 10 | import io.flutter.embedding.engine.FlutterEngine 11 | import io.flutter.embedding.engine.dart.DartExecutor 12 | import java.util.concurrent.locks.ReentrantLock 13 | import kotlin.concurrent.withLock 14 | 15 | enum class RunState { 16 | START, 17 | PENDING, 18 | STOP 19 | } 20 | 21 | 22 | object GlobalState { 23 | 24 | private val lock = ReentrantLock() 25 | val runLock = ReentrantLock() 26 | 27 | val runState: MutableLiveData = MutableLiveData(RunState.STOP) 28 | var flutterEngine: FlutterEngine? = null 29 | private var serviceEngine: FlutterEngine? = null 30 | 31 | fun getCurrentAppPlugin(): AppPlugin? { 32 | val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine 33 | return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin? 34 | } 35 | 36 | fun getCurrentTilePlugin(): TilePlugin? { 37 | val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine 38 | return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin? 39 | } 40 | 41 | fun getCurrentVPNPlugin(): VpnPlugin? { 42 | return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin? 43 | } 44 | 45 | fun destroyServiceEngine() { 46 | serviceEngine?.destroy() 47 | serviceEngine = null 48 | } 49 | 50 | fun initServiceEngine(context: Context) { 51 | if (serviceEngine != null) return 52 | lock.withLock { 53 | destroyServiceEngine() 54 | serviceEngine = FlutterEngine(context) 55 | serviceEngine?.plugins?.add(VpnPlugin()) 56 | serviceEngine?.plugins?.add(AppPlugin()) 57 | serviceEngine?.plugins?.add(TilePlugin()) 58 | serviceEngine?.plugins?.add(ServicePlugin()) 59 | val vpnService = DartExecutor.DartEntrypoint( 60 | FlutterInjector.instance().flutterLoader().findAppBundlePath(), 61 | "vpnService" 62 | ) 63 | serviceEngine?.dartExecutor?.executeDartEntrypoint( 64 | vpnService, 65 | ) 66 | } 67 | } 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/follow/clash/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.follow.clash 2 | 3 | 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import com.follow.clash.plugins.AppPlugin 7 | import com.follow.clash.plugins.ServicePlugin 8 | import com.follow.clash.plugins.VpnPlugin 9 | import com.follow.clash.plugins.TilePlugin 10 | import io.flutter.embedding.android.FlutterActivity 11 | import io.flutter.embedding.engine.FlutterEngine 12 | 13 | class MainActivity : FlutterActivity() { 14 | override fun configureFlutterEngine(flutterEngine: FlutterEngine) { 15 | super.configureFlutterEngine(flutterEngine) 16 | flutterEngine.plugins.add(AppPlugin()) 17 | flutterEngine.plugins.add(VpnPlugin()) 18 | flutterEngine.plugins.add(ServicePlugin()) 19 | flutterEngine.plugins.add(TilePlugin()) 20 | GlobalState.flutterEngine = flutterEngine 21 | } 22 | 23 | override fun onDestroy() { 24 | GlobalState.flutterEngine = null 25 | super.onDestroy() 26 | } 27 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/follow/clash/TempActivity.kt: -------------------------------------------------------------------------------- 1 | package com.follow.clash 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | 6 | class TempActivity : Activity() { 7 | override fun onCreate(savedInstanceState: Bundle?) { 8 | super.onCreate(savedInstanceState) 9 | when (intent.action) { 10 | "com.follow.clash.action.START" -> { 11 | GlobalState.getCurrentTilePlugin()?.handleStart() 12 | } 13 | 14 | "com.follow.clash.action.STOP" -> { 15 | GlobalState.getCurrentTilePlugin()?.handleStop() 16 | } 17 | } 18 | finishAndRemoveTask() 19 | } 20 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt: -------------------------------------------------------------------------------- 1 | package com.follow.clash.extensions 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.drawable.Drawable 5 | import android.net.ConnectivityManager 6 | import android.net.Network 7 | import android.system.OsConstants.IPPROTO_TCP 8 | import android.system.OsConstants.IPPROTO_UDP 9 | import android.util.Base64 10 | import androidx.core.graphics.drawable.toBitmap 11 | import com.follow.clash.models.CIDR 12 | import com.follow.clash.models.Metadata 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.withContext 15 | import java.io.ByteArrayOutputStream 16 | import java.net.Inet4Address 17 | import java.net.Inet6Address 18 | import java.net.InetAddress 19 | 20 | 21 | suspend fun Drawable.getBase64(): String { 22 | val drawable = this 23 | return withContext(Dispatchers.IO) { 24 | val bitmap = drawable.toBitmap() 25 | val byteArrayOutputStream = ByteArrayOutputStream() 26 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) 27 | Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP) 28 | } 29 | } 30 | 31 | fun Metadata.getProtocol(): Int? { 32 | if (network.startsWith("tcp")) return IPPROTO_TCP 33 | if (network.startsWith("udp")) return IPPROTO_UDP 34 | return null 35 | } 36 | 37 | fun String.toCIDR(): CIDR { 38 | val parts = split("/") 39 | if (parts.size != 2) { 40 | throw IllegalArgumentException("Invalid CIDR format") 41 | } 42 | val ipAddress = parts[0] 43 | val prefixLength = parts[1].toIntOrNull() 44 | ?: throw IllegalArgumentException("Invalid prefix length") 45 | 46 | val address = InetAddress.getByName(ipAddress) 47 | 48 | val maxPrefix = if (address.address.size == 4) 32 else 128 49 | if (prefixLength < 0 || prefixLength > maxPrefix) { 50 | throw IllegalArgumentException("Invalid prefix length for IP version") 51 | } 52 | 53 | return CIDR(address, prefixLength) 54 | } 55 | 56 | 57 | fun ConnectivityManager.resolveDns(network: Network?): List { 58 | val properties = getLinkProperties(network) ?: return listOf() 59 | return properties.dnsServers.map { it.asSocketAddressText(53) } 60 | } 61 | 62 | fun InetAddress.asSocketAddressText(port: Int): String { 63 | return when (this) { 64 | is Inet6Address -> 65 | "[${numericToTextFormat(this.address)}]:$port" 66 | 67 | is Inet4Address -> 68 | "${this.hostAddress}:$port" 69 | 70 | else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}") 71 | } 72 | } 73 | 74 | 75 | private fun numericToTextFormat(src: ByteArray): String { 76 | val sb = StringBuilder(39) 77 | for (i in 0 until 8) { 78 | sb.append( 79 | Integer.toHexString( 80 | src[i shl 1].toInt() shl 8 and 0xff00 81 | or (src[(i shl 1) + 1].toInt() and 0xff) 82 | ) 83 | ) 84 | if (i < 7) { 85 | sb.append(":") 86 | } 87 | } 88 | return sb.toString() 89 | } 90 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/follow/clash/models/Package.kt: -------------------------------------------------------------------------------- 1 | package com.follow.clash.models 2 | 3 | data class Package( 4 | val packageName: String, 5 | val label: String, 6 | val isSystem: Boolean, 7 | val firstInstallTime: Long, 8 | ) 9 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/follow/clash/models/Process.kt: -------------------------------------------------------------------------------- 1 | package com.follow.clash.models 2 | 3 | data class Process( 4 | val id: Int, 5 | val metadata: Metadata, 6 | ) 7 | 8 | data class Metadata( 9 | val network: String, 10 | val sourceIP: String, 11 | val sourcePort: Int, 12 | val destinationIP: String, 13 | val destinationPort: Int, 14 | val host: String 15 | ) 16 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/follow/clash/models/Props.kt: -------------------------------------------------------------------------------- 1 | package com.follow.clash.models 2 | 3 | import java.net.InetAddress 4 | 5 | enum class AccessControlMode { 6 | acceptSelected, 7 | rejectSelected, 8 | } 9 | 10 | data class AccessControl( 11 | val mode: AccessControlMode, 12 | val acceptList: List, 13 | val rejectList: List, 14 | ) 15 | 16 | data class CIDR(val address: InetAddress, val prefixLength: Int) 17 | 18 | data class VpnOptions( 19 | val enable: Boolean, 20 | val port: Int, 21 | val accessControl: AccessControl?, 22 | val allowBypass: Boolean, 23 | val systemProxy: Boolean, 24 | val bypassDomain: List, 25 | val ipv4Address: String, 26 | val ipv6Address: String, 27 | val dnsServerAddress: String, 28 | ) -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.follow.clash.plugins 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import androidx.core.content.getSystemService 6 | import com.follow.clash.GlobalState 7 | import io.flutter.embedding.engine.plugins.FlutterPlugin 8 | import io.flutter.plugin.common.MethodCall 9 | import io.flutter.plugin.common.MethodChannel 10 | 11 | 12 | class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { 13 | 14 | private lateinit var flutterMethodChannel: MethodChannel 15 | 16 | private lateinit var context: Context 17 | 18 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 19 | context = flutterPluginBinding.applicationContext 20 | flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service") 21 | flutterMethodChannel.setMethodCallHandler(this) 22 | } 23 | 24 | override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 25 | flutterMethodChannel.setMethodCallHandler(null) 26 | } 27 | 28 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) { 29 | "init" -> { 30 | GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context) 31 | GlobalState.initServiceEngine(context) 32 | result.success(true) 33 | } 34 | 35 | "destroy" -> { 36 | handleDestroy() 37 | result.success(true) 38 | } 39 | 40 | else -> { 41 | result.notImplemented() 42 | } 43 | } 44 | 45 | private fun handleDestroy() { 46 | GlobalState.getCurrentVPNPlugin()?.stop() 47 | GlobalState.destroyServiceEngine() 48 | } 49 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt: -------------------------------------------------------------------------------- 1 | 2 | package com.follow.clash.plugins 3 | 4 | import io.flutter.embedding.engine.plugins.FlutterPlugin 5 | import io.flutter.plugin.common.MethodCall 6 | import io.flutter.plugin.common.MethodChannel 7 | 8 | class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop: (() -> Unit)? = null) : FlutterPlugin, 9 | MethodChannel.MethodCallHandler { 10 | 11 | private lateinit var channel: MethodChannel 12 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 13 | channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile") 14 | channel.setMethodCallHandler(this) 15 | } 16 | 17 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { 18 | handleDetached() 19 | channel.setMethodCallHandler(null) 20 | } 21 | 22 | fun handleStart() { 23 | onStart?.let { it() } 24 | channel.invokeMethod("start", null) 25 | } 26 | 27 | fun handleStop() { 28 | channel.invokeMethod("stop", null) 29 | onStop?.let { it() } 30 | } 31 | 32 | private fun handleDetached() { 33 | channel.invokeMethod("detached", null) 34 | } 35 | 36 | 37 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {} 38 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_stat_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/drawable-hdpi/ic_stat_name.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_stat_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/drawable-mdpi/ic_stat_name.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_stat_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/drawable-xhdpi/ic_stat_name.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_stat_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/drawable-xxhdpi/ic_stat_name.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 17 | 20 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/values-night-v27/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-v27/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FAFAFA 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | FlClash 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 14 | 17 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | localhost 12 | 127.0.0.1 13 | 14 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = "${kotlin_version}" 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath "com.android.tools.build:gradle:$agp_version" 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | 33 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | kotlin_version=1.9.22 5 | agp_version=8.2.1 6 | -------------------------------------------------------------------------------- /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-8.2-bin.zip 6 | 7 | -------------------------------------------------------------------------------- /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 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version "$agp_version" apply false 23 | id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false 24 | } 25 | 26 | include ":app" 27 | -------------------------------------------------------------------------------- /assets/data/ASN.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/data/ASN.mmdb -------------------------------------------------------------------------------- /assets/data/GeoIP.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/data/GeoIP.dat -------------------------------------------------------------------------------- /assets/data/GeoSite.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/data/GeoSite.dat -------------------------------------------------------------------------------- /assets/data/geoip.metadb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/data/geoip.metadb -------------------------------------------------------------------------------- /assets/fonts/Icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/fonts/Icons.ttf -------------------------------------------------------------------------------- /assets/fonts/Twemoji.Mozilla.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/fonts/Twemoji.Mozilla.ttf -------------------------------------------------------------------------------- /assets/images/avatars/arue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/images/avatars/arue.jpg -------------------------------------------------------------------------------- /assets/images/avatars/june2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/images/avatars/june2.jpg -------------------------------------------------------------------------------- /assets/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/images/icon.ico -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/icon_black.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/images/icon_black.ico -------------------------------------------------------------------------------- /assets/images/icon_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/images/icon_black.png -------------------------------------------------------------------------------- /assets/images/icon_white.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/images/icon_white.ico -------------------------------------------------------------------------------- /assets/images/icon_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/assets/images/icon_white.png -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | source_gen:combining_builder: 5 | options: 6 | build_extensions: 7 | '^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart' 8 | freezed: 9 | options: 10 | build_extensions: 11 | '^lib/models/{{}}.dart': 'lib/models/generated/{{}}.freezed.dart' -------------------------------------------------------------------------------- /core/dart-bridge/include/dart_api_dl.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 3 | * for details. All rights reserved. Use of this source code is governed by a 4 | * BSD-style license that can be found in the LICENSE file. 5 | */ 6 | 7 | #include "dart_api_dl.h" /* NOLINT */ 8 | #include "dart_version.h" /* NOLINT */ 9 | #include "internal/dart_api_dl_impl.h" /* NOLINT */ 10 | 11 | #include 12 | 13 | #define DART_API_DL_DEFINITIONS(name, R, A) name##_Type name##_DL = NULL; 14 | 15 | DART_API_ALL_DL_SYMBOLS(DART_API_DL_DEFINITIONS) 16 | 17 | #undef DART_API_DL_DEFINITIONS 18 | 19 | typedef void* DartApiEntry_function; 20 | 21 | DartApiEntry_function FindFunctionPointer(const DartApiEntry* entries, 22 | const char* name) { 23 | while (entries->name != NULL) { 24 | if (strcmp(entries->name, name) == 0) return entries->function; 25 | entries++; 26 | } 27 | return NULL; 28 | } 29 | 30 | intptr_t Dart_InitializeApiDL(void* data) { 31 | DartApi* dart_api_data = (DartApi*)data; 32 | 33 | if (dart_api_data->major != DART_API_DL_MAJOR_VERSION) { 34 | // If the DartVM we're running on does not have the same version as this 35 | // file was compiled against, refuse to initialize. The symbols are not 36 | // compatible. 37 | return -1; 38 | } 39 | // Minor versions are allowed to be different. 40 | // If the DartVM has a higher minor version, it will provide more symbols 41 | // than we initialize here. 42 | // If the DartVM has a lower minor version, it will not provide all symbols. 43 | // In that case, we leave the missing symbols un-initialized. Those symbols 44 | // should not be used by the Dart and native code. The client is responsible 45 | // for checking the minor version number himself based on which symbols it 46 | // is using. 47 | // (If we would error out on this case, recompiling native code against a 48 | // newer SDK would break all uses on older SDKs, which is too strict.) 49 | 50 | const DartApiEntry* dart_api_function_pointers = dart_api_data->functions; 51 | 52 | #define DART_API_DL_INIT(name, R, A) \ 53 | name##_DL = \ 54 | (name##_Type)(FindFunctionPointer(dart_api_function_pointers, #name)); 55 | DART_API_ALL_DL_SYMBOLS(DART_API_DL_INIT) 56 | #undef DART_API_DL_INIT 57 | 58 | return 0; 59 | } 60 | -------------------------------------------------------------------------------- /core/dart-bridge/include/dart_version.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 3 | * for details. All rights reserved. Use of this source code is governed by a 4 | * BSD-style license that can be found in the LICENSE file. 5 | */ 6 | 7 | #ifndef RUNTIME_INCLUDE_DART_VERSION_H_ 8 | #define RUNTIME_INCLUDE_DART_VERSION_H_ 9 | 10 | // On breaking changes the major version is increased. 11 | // On backwards compatible changes the minor version is increased. 12 | // The versioning covers the symbols exposed in dart_api_dl.h 13 | #define DART_API_DL_MAJOR_VERSION 2 14 | #define DART_API_DL_MINOR_VERSION 3 15 | 16 | #endif /* RUNTIME_INCLUDE_DART_VERSION_H_ */ /* NOLINT */ 17 | -------------------------------------------------------------------------------- /core/dart-bridge/include/internal/dart_api_dl_impl.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 3 | * for details. All rights reserved. Use of this source code is governed by a 4 | * BSD-style license that can be found in the LICENSE file. 5 | */ 6 | 7 | #ifndef RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_ 8 | #define RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_ 9 | 10 | typedef struct { 11 | const char* name; 12 | void (*function)(void); 13 | } DartApiEntry; 14 | 15 | typedef struct { 16 | const int major; 17 | const int minor; 18 | const DartApiEntry* const functions; 19 | } DartApi; 20 | 21 | #endif /* RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_ */ /* NOLINT */ 22 | -------------------------------------------------------------------------------- /core/dart-bridge/lib.go: -------------------------------------------------------------------------------- 1 | package dart_bridge 2 | 3 | /* 4 | #include 5 | #include "stdint.h" 6 | #include "include/dart_api_dl.h" 7 | #include "include/dart_api_dl.c" 8 | #include "include/dart_native_api.h" 9 | 10 | bool GoDart_PostCObject(Dart_Port_DL port, Dart_CObject* obj) { 11 | return Dart_PostCObject_DL(port, obj); 12 | } 13 | */ 14 | import "C" 15 | import ( 16 | "fmt" 17 | "unsafe" 18 | ) 19 | 20 | func InitDartApi(api unsafe.Pointer) { 21 | if C.Dart_InitializeApiDL(api) != 0 { 22 | panic("failed to create dart bridge") 23 | } else { 24 | fmt.Println("Dart Api DL is initialized") 25 | } 26 | } 27 | 28 | func SendToPort(port int64, msg string) bool { 29 | var obj C.Dart_CObject 30 | obj._type = C.Dart_CObject_kString 31 | msgString := C.CString(msg) 32 | defer C.free(unsafe.Pointer(msgString)) 33 | ptr := unsafe.Pointer(&obj.value[0]) 34 | *(**C.char)(ptr) = msgString 35 | isSuccess := C.GoDart_PostCObject(C.Dart_Port_DL(port), &obj) 36 | if !isSuccess { 37 | return false 38 | } 39 | return true 40 | } 41 | -------------------------------------------------------------------------------- /core/dns.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | 3 | package main 4 | 5 | import "C" 6 | import ( 7 | "github.com/metacubex/mihomo/dns" 8 | "github.com/metacubex/mihomo/log" 9 | "strings" 10 | ) 11 | 12 | //export updateDns 13 | func updateDns(s *C.char) { 14 | dnsList := C.GoString(s) 15 | go func() { 16 | log.Infoln("[DNS] updateDns %s", dnsList) 17 | dns.UpdateSystemDNS(strings.Split(dnsList, ",")) 18 | dns.FlushCacheWithDefaultResolver() 19 | }() 20 | } 21 | -------------------------------------------------------------------------------- /core/log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "C" 4 | import ( 5 | "github.com/metacubex/mihomo/common/observable" 6 | "github.com/metacubex/mihomo/log" 7 | ) 8 | 9 | var logSubscriber observable.Subscription[log.Event] 10 | 11 | //export startLog 12 | func startLog() { 13 | if logSubscriber != nil { 14 | log.UnSubscribe(logSubscriber) 15 | logSubscriber = nil 16 | } 17 | logSubscriber = log.Subscribe() 18 | go func() { 19 | for logData := range logSubscriber { 20 | if logData.LogLevel < log.Level() { 21 | continue 22 | } 23 | message := &Message{ 24 | Type: LogMessage, 25 | Data: logData, 26 | } 27 | SendMessage(*message) 28 | } 29 | }() 30 | } 31 | 32 | //export stopLog 33 | func stopLog() { 34 | if logSubscriber != nil { 35 | log.UnSubscribe(logSubscriber) 36 | logSubscriber = nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "C" 4 | import ( 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | fmt.Println("init clash") 10 | } 11 | -------------------------------------------------------------------------------- /core/message.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | bridge "core/dart-bridge" 5 | "encoding/json" 6 | "github.com/metacubex/mihomo/constant" 7 | ) 8 | 9 | var Port int64 10 | var ServicePort int64 11 | 12 | type MessageType string 13 | 14 | const ( 15 | LogMessage MessageType = "log" 16 | ProtectMessage MessageType = "protect" 17 | DelayMessage MessageType = "delay" 18 | ProcessMessage MessageType = "process" 19 | RequestMessage MessageType = "request" 20 | StartedMessage MessageType = "started" 21 | LoadedMessage MessageType = "loaded" 22 | ) 23 | 24 | type Delay struct { 25 | Name string `json:"name"` 26 | Value int32 `json:"value"` 27 | } 28 | 29 | type Process struct { 30 | Id int64 `json:"id"` 31 | Metadata *constant.Metadata `json:"metadata"` 32 | } 33 | 34 | type Message struct { 35 | Type MessageType `json:"type"` 36 | Data interface{} `json:"data"` 37 | } 38 | 39 | func (message *Message) Json() (string, error) { 40 | data, err := json.Marshal(message) 41 | return string(data), err 42 | } 43 | 44 | func SendMessage(message Message) { 45 | s, err := message.Json() 46 | if err != nil { 47 | return 48 | } 49 | if handler, ok := messageHandlers[message.Type]; ok { 50 | handler(s) 51 | } else { 52 | sendToPort(s) 53 | } 54 | } 55 | 56 | var messageHandlers = map[MessageType]func(string) bool{ 57 | ProtectMessage: sendToServicePort, 58 | ProcessMessage: sendToServicePort, 59 | StartedMessage: conditionalSend, 60 | LoadedMessage: conditionalSend, 61 | } 62 | 63 | func sendToPort(s string) bool { 64 | return bridge.SendToPort(Port, s) 65 | } 66 | 67 | func sendToServicePort(s string) bool { 68 | return bridge.SendToPort(ServicePort, s) 69 | } 70 | 71 | func conditionalSend(s string) bool { 72 | isSuccess := sendToPort(s) 73 | if !isSuccess { 74 | return sendToServicePort(s) 75 | } 76 | return isSuccess 77 | } 78 | -------------------------------------------------------------------------------- /core/platform/limit.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | 3 | package platform 4 | 5 | import "syscall" 6 | 7 | var nullFd int 8 | var maxFdCount int 9 | 10 | func init() { 11 | fd, err := syscall.Open("/dev/null", syscall.O_WRONLY, 0644) 12 | if err != nil { 13 | panic(err.Error()) 14 | } 15 | 16 | nullFd = fd 17 | 18 | var limit syscall.Rlimit 19 | 20 | if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil { 21 | maxFdCount = 1024 22 | } else { 23 | maxFdCount = int(limit.Cur) 24 | } 25 | 26 | maxFdCount = maxFdCount / 4 * 3 27 | } 28 | 29 | func ShouldBlockConnection() bool { 30 | fd, err := syscall.Dup(nullFd) 31 | if err != nil { 32 | return true 33 | } 34 | 35 | _ = syscall.Close(fd) 36 | 37 | if fd > maxFdCount { 38 | return true 39 | } 40 | 41 | return false 42 | } 43 | -------------------------------------------------------------------------------- /core/process.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | 3 | package main 4 | 5 | import "C" 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "github.com/metacubex/mihomo/component/process" 10 | "github.com/metacubex/mihomo/constant" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | ) 15 | 16 | type ProcessMap struct { 17 | m sync.Map 18 | } 19 | 20 | func (cm *ProcessMap) Store(key int64, value string) { 21 | cm.m.Store(key, value) 22 | } 23 | 24 | func (cm *ProcessMap) Load(key int64) (string, bool) { 25 | value, ok := cm.m.Load(key) 26 | if !ok || value == nil { 27 | return "", false 28 | } 29 | return value.(string), true 30 | } 31 | 32 | var counter int64 = 0 33 | 34 | var processMap ProcessMap 35 | 36 | func init() { 37 | process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) { 38 | if metadata == nil { 39 | return "", process.ErrInvalidNetwork 40 | } 41 | id := atomic.AddInt64(&counter, 1) 42 | 43 | timeout := time.After(200 * time.Millisecond) 44 | 45 | SendMessage(Message{ 46 | Type: ProcessMessage, 47 | Data: Process{ 48 | Id: id, 49 | Metadata: metadata, 50 | }, 51 | }) 52 | 53 | for { 54 | select { 55 | case <-timeout: 56 | return "", errors.New("package resolver timeout") 57 | default: 58 | value, exists := processMap.Load(id) 59 | if exists { 60 | return value, nil 61 | } 62 | time.Sleep(20 * time.Millisecond) 63 | } 64 | } 65 | } 66 | } 67 | 68 | //export setProcessMap 69 | func setProcessMap(s *C.char) { 70 | if s == nil { 71 | return 72 | } 73 | paramsString := C.GoString(s) 74 | go func() { 75 | var processMapItem = &ProcessMapItem{} 76 | err := json.Unmarshal([]byte(paramsString), processMapItem) 77 | if err == nil { 78 | processMap.Store(processMapItem.Id, processMapItem.Value) 79 | } 80 | }() 81 | } 82 | -------------------------------------------------------------------------------- /core/state.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "C" 4 | import ( 5 | "core/state" 6 | "encoding/json" 7 | "fmt" 8 | ) 9 | 10 | //export getCurrentProfileName 11 | func getCurrentProfileName() *C.char { 12 | if state.CurrentState == nil { 13 | return C.CString("") 14 | } 15 | return C.CString(state.CurrentState.CurrentProfileName) 16 | } 17 | 18 | //export getAndroidVpnOptions 19 | func getAndroidVpnOptions() *C.char { 20 | options := state.AndroidVpnOptions{ 21 | Enable: state.CurrentState.Enable, 22 | Port: state.CurrentRawConfig.MixedPort, 23 | Ipv4Address: state.DefaultIpv4Address, 24 | Ipv6Address: state.GetIpv6Address(), 25 | AccessControl: state.CurrentState.AccessControl, 26 | SystemProxy: state.CurrentState.SystemProxy, 27 | AllowBypass: state.CurrentState.AllowBypass, 28 | BypassDomain: state.CurrentState.BypassDomain, 29 | DnsServerAddress: state.GetDnsServerAddress(), 30 | } 31 | data, err := json.Marshal(options) 32 | if err != nil { 33 | fmt.Println("Error:", err) 34 | return C.CString("") 35 | } 36 | return C.CString(string(data)) 37 | } 38 | 39 | //export setState 40 | func setState(s *C.char) { 41 | paramsString := C.GoString(s) 42 | err := json.Unmarshal([]byte(paramsString), state.CurrentState) 43 | if err != nil { 44 | return 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import "github.com/metacubex/mihomo/config" 4 | 5 | var DefaultIpv4Address = "172.19.0.1/30" 6 | var DefaultDnsAddress = "172.19.0.2" 7 | var DefaultIpv6Address = "fdfe:dcba:9876::1/126" 8 | 9 | var CurrentRawConfig = config.DefaultRawConfig() 10 | 11 | type AndroidVpnOptions struct { 12 | Enable bool `json:"enable"` 13 | Port int `json:"port"` 14 | AccessControl *AccessControl `json:"accessControl"` 15 | AllowBypass bool `json:"allowBypass"` 16 | SystemProxy bool `json:"systemProxy"` 17 | BypassDomain []string `json:"bypassDomain"` 18 | Ipv4Address string `json:"ipv4Address"` 19 | Ipv6Address string `json:"ipv6Address"` 20 | DnsServerAddress string `json:"dnsServerAddress"` 21 | } 22 | 23 | type AccessControl struct { 24 | Mode string `json:"mode"` 25 | AcceptList []string `json:"acceptList"` 26 | RejectList []string `json:"rejectList"` 27 | IsFilterSystemApp bool `json:"isFilterSystemApp"` 28 | } 29 | 30 | type AndroidVpnRawOptions struct { 31 | Enable bool `json:"enable"` 32 | AccessControl *AccessControl `json:"accessControl"` 33 | AllowBypass bool `json:"allowBypass"` 34 | SystemProxy bool `json:"systemProxy"` 35 | Ipv6 bool `json:"ipv6"` 36 | BypassDomain []string `json:"bypassDomain"` 37 | } 38 | 39 | type State struct { 40 | AndroidVpnRawOptions 41 | CurrentProfileName string `json:"currentProfileName"` 42 | OnlyProxy bool `json:"onlyProxy"` 43 | } 44 | 45 | var CurrentState = &State{} 46 | 47 | func GetIpv6Address() string { 48 | if CurrentState.Ipv6 { 49 | return DefaultIpv6Address 50 | } else { 51 | return "" 52 | } 53 | } 54 | 55 | func GetDnsServerAddress() string { 56 | //prefix, _ := netip.ParsePrefix(DefaultIpv4Address) 57 | //return prefix.Addr().String() 58 | return DefaultDnsAddress 59 | } 60 | -------------------------------------------------------------------------------- /core/tun/tun.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | 3 | package tun 4 | 5 | import "C" 6 | import ( 7 | "core/state" 8 | LC "github.com/metacubex/mihomo/listener/config" 9 | "github.com/metacubex/mihomo/listener/sing_tun" 10 | "github.com/metacubex/mihomo/log" 11 | "github.com/metacubex/mihomo/tunnel" 12 | "net" 13 | "net/netip" 14 | ) 15 | 16 | type Props struct { 17 | Fd int `json:"fd"` 18 | Gateway string `json:"gateway"` 19 | Gateway6 string `json:"gateway6"` 20 | Portal string `json:"portal"` 21 | Portal6 string `json:"portal6"` 22 | Dns string `json:"dns"` 23 | Dns6 string `json:"dns6"` 24 | } 25 | 26 | func Start(fd int) (*sing_tun.Listener, error) { 27 | var prefix4 []netip.Prefix 28 | tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address) 29 | if err != nil { 30 | log.Errorln("startTUN error:", err) 31 | return nil, err 32 | } 33 | prefix4 = append(prefix4, tempPrefix4) 34 | var prefix6 []netip.Prefix 35 | if state.CurrentState.Ipv6 { 36 | tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address) 37 | if err != nil { 38 | log.Errorln("startTUN error:", err) 39 | return nil, err 40 | } 41 | prefix6 = append(prefix6, tempPrefix6) 42 | } 43 | 44 | var dnsHijack []string 45 | dnsHijack = append(dnsHijack, net.JoinHostPort(state.GetDnsServerAddress(), "53")) 46 | 47 | options := LC.Tun{ 48 | Enable: true, 49 | Device: state.CurrentRawConfig.Tun.Device, 50 | Stack: state.CurrentRawConfig.Tun.Stack, 51 | DNSHijack: dnsHijack, 52 | AutoRoute: false, 53 | AutoDetectInterface: false, 54 | Inet4Address: prefix4, 55 | Inet6Address: prefix6, 56 | MTU: 9000, 57 | FileDescriptor: fd, 58 | } 59 | 60 | listener, err := sing_tun.New(options, tunnel.Tunnel) 61 | 62 | if err != nil { 63 | log.Errorln("startTUN error:", err) 64 | return nil, err 65 | } 66 | 67 | return listener, nil 68 | } 69 | -------------------------------------------------------------------------------- /distribute_options.yaml: -------------------------------------------------------------------------------- 1 | app_name: 'FlClash' 2 | output: 'dist/' 3 | -------------------------------------------------------------------------------- /lib/clash/clash.dart: -------------------------------------------------------------------------------- 1 | export 'core.dart'; 2 | export 'service.dart'; 3 | export 'message.dart'; -------------------------------------------------------------------------------- /lib/clash/message.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:fl_clash/enum/enum.dart'; 5 | import 'package:fl_clash/models/models.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | 8 | import 'core.dart'; 9 | 10 | class ClashMessage { 11 | StreamSubscription? subscription; 12 | 13 | ClashMessage._() { 14 | if (subscription != null) { 15 | subscription!.cancel(); 16 | subscription = null; 17 | } 18 | subscription = ClashCore.receiver.listen((message) { 19 | final m = AppMessage.fromJson(json.decode(message)); 20 | for (final AppMessageListener listener in _listeners) { 21 | switch (m.type) { 22 | case AppMessageType.log: 23 | listener.onLog(Log.fromJson(m.data)); 24 | break; 25 | case AppMessageType.delay: 26 | listener.onDelay(Delay.fromJson(m.data)); 27 | break; 28 | case AppMessageType.request: 29 | listener.onRequest(Connection.fromJson(m.data)); 30 | break; 31 | case AppMessageType.started: 32 | listener.onStarted(m.data); 33 | break; 34 | case AppMessageType.loaded: 35 | listener.onLoaded(m.data); 36 | break; 37 | } 38 | } 39 | }); 40 | } 41 | 42 | static final ClashMessage instance = ClashMessage._(); 43 | 44 | final ObserverList _listeners = 45 | ObserverList(); 46 | 47 | bool get hasListeners { 48 | return _listeners.isNotEmpty; 49 | } 50 | 51 | void addListener(AppMessageListener listener) { 52 | _listeners.add(listener); 53 | } 54 | 55 | void removeListener(AppMessageListener listener) { 56 | _listeners.remove(listener); 57 | } 58 | } 59 | 60 | final clashMessage = ClashMessage.instance; 61 | -------------------------------------------------------------------------------- /lib/clash/service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:fl_clash/common/common.dart'; 3 | import 'package:fl_clash/models/models.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:path/path.dart'; 7 | 8 | import 'core.dart'; 9 | 10 | class ClashService { 11 | Future initGeo() async { 12 | final homePath = await appPath.getHomeDirPath(); 13 | final homeDir = Directory(homePath); 14 | final isExists = await homeDir.exists(); 15 | if (!isExists) { 16 | await homeDir.create(recursive: true); 17 | } 18 | const geoFileNameList = [ 19 | mmdbFileName, 20 | geoIpFileName, 21 | geoSiteFileName, 22 | asnFileName, 23 | ]; 24 | try { 25 | for (final geoFileName in geoFileNameList) { 26 | final geoFile = File( 27 | join(homePath, geoFileName), 28 | ); 29 | final isExists = await geoFile.exists(); 30 | if (isExists) { 31 | continue; 32 | } 33 | final data = await rootBundle.load('assets/data/$geoFileName'); 34 | List bytes = data.buffer.asUint8List(); 35 | await geoFile.writeAsBytes(bytes, flush: true); 36 | } 37 | } catch (e) { 38 | debugPrint("$e"); 39 | exit(0); 40 | } 41 | } 42 | 43 | Future init({ 44 | required ClashConfig clashConfig, 45 | required Config config, 46 | }) async { 47 | await initGeo(); 48 | final homeDirPath = await appPath.getHomeDirPath(); 49 | final isInit = clashCore.init(homeDirPath); 50 | return isInit; 51 | } 52 | } 53 | 54 | final clashService = ClashService(); 55 | -------------------------------------------------------------------------------- /lib/common/android.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:fl_clash/plugins/app.dart'; 4 | import 'package:fl_clash/state.dart'; 5 | 6 | class Android { 7 | init() async { 8 | app?.onExit = () async { 9 | await globalState.appController.savePreferences(); 10 | }; 11 | } 12 | } 13 | 14 | final android = Platform.isAndroid ? Android() : null; 15 | -------------------------------------------------------------------------------- /lib/common/app_localizations.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/l10n/l10n.dart'; 2 | 3 | final appLocalizations = AppLocalizations.current; -------------------------------------------------------------------------------- /lib/common/archive.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:archive/archive_io.dart'; 4 | import 'package:path/path.dart'; 5 | 6 | extension ArchiveExt on Archive { 7 | addDirectoryToArchive(String dirPath, String parentPath) { 8 | final dir = Directory(dirPath); 9 | final entities = dir.listSync(recursive: false); 10 | for (final entity in entities) { 11 | final relativePath = relative(entity.path, from: parentPath); 12 | if (entity is File) { 13 | final data = entity.readAsBytesSync(); 14 | final archiveFile = ArchiveFile(relativePath, data.length, data); 15 | addFile(archiveFile); 16 | } else if (entity is Directory) { 17 | addDirectoryToArchive(entity.path, parentPath); 18 | } 19 | } 20 | } 21 | 22 | add(String name, T raw) { 23 | final data = json.encode(raw); 24 | addFile( 25 | ArchiveFile(name, data.length, data), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/common/color.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension ColorExtension on Color { 4 | toLight() { 5 | return withOpacity(0.6); 6 | } 7 | 8 | toLighter() { 9 | return withOpacity(0.4); 10 | } 11 | 12 | toSoft() { 13 | return withOpacity(0.12); 14 | } 15 | 16 | toLittle() { 17 | return withOpacity(0.03); 18 | } 19 | 20 | Color darken([double amount = .1]) { 21 | assert(amount >= 0 && amount <= 1); 22 | final hsl = HSLColor.fromColor(this); 23 | final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); 24 | return hslDark.toColor(); 25 | } 26 | } 27 | 28 | extension ColorSchemeExtension on ColorScheme { 29 | ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack 30 | ? copyWith( 31 | surface: Colors.black, 32 | background: Colors.black, 33 | surfaceContainer: surfaceContainer.darken(0.05), 34 | ) 35 | : this; 36 | } 37 | -------------------------------------------------------------------------------- /lib/common/common.dart: -------------------------------------------------------------------------------- 1 | export 'path.dart'; 2 | export 'request.dart'; 3 | export 'preferences.dart'; 4 | export 'constant.dart'; 5 | export 'proxy.dart'; 6 | export 'other.dart'; 7 | export 'num.dart'; 8 | export 'navigation.dart'; 9 | export 'window.dart'; 10 | export 'system.dart'; 11 | export 'picker.dart'; 12 | export 'android.dart'; 13 | export 'launch.dart'; 14 | export 'protocol.dart'; 15 | export 'datetime.dart'; 16 | export 'context.dart'; 17 | export 'link.dart'; 18 | export 'text.dart'; 19 | export 'color.dart'; 20 | export 'list.dart'; 21 | export 'string.dart'; 22 | export 'app_localizations.dart'; 23 | export 'function.dart'; 24 | export 'package.dart'; 25 | export 'measure.dart'; 26 | export 'windows.dart'; 27 | export 'iterable.dart'; 28 | export 'scroll.dart'; 29 | export 'icons.dart'; 30 | export 'http.dart'; 31 | export 'keyboard.dart'; 32 | export 'network.dart'; -------------------------------------------------------------------------------- /lib/common/constant.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:ui'; 3 | 4 | import 'package:collection/collection.dart'; 5 | import 'package:fl_clash/enum/enum.dart'; 6 | import 'package:fl_clash/models/models.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'system.dart'; 9 | 10 | const appName = "FlClash"; 11 | const coreName = "clash.meta"; 12 | const packageName = "com.follow.clash"; 13 | const httpTimeoutDuration = Duration(milliseconds: 5000); 14 | const moreDuration = Duration(milliseconds: 100); 15 | const animateDuration = Duration(milliseconds: 100); 16 | const defaultUpdateDuration = Duration(days: 1); 17 | const mmdbFileName = "geoip.metadb"; 18 | const asnFileName = "ASN.mmdb"; 19 | const geoIpFileName = "GeoIP.dat"; 20 | const geoSiteFileName = "GeoSite.dat"; 21 | final double kHeaderHeight = system.isDesktop 22 | ? !Platform.isMacOS 23 | ? 40 24 | : 26 25 | : 0; 26 | const GeoXMap defaultGeoXMap = { 27 | "mmdb": 28 | "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", 29 | "asn": 30 | "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb", 31 | "geoip": 32 | "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", 33 | "geosite": 34 | "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat" 35 | }; 36 | const profilesDirectoryName = "profiles"; 37 | const localhost = "127.0.0.1"; 38 | const clashConfigKey = "clash_config"; 39 | const configKey = "config"; 40 | const listItemPadding = EdgeInsets.symmetric(horizontal: 16); 41 | const double dialogCommonWidth = 300; 42 | const repository = "chen08209/FlClash"; 43 | const defaultExternalController = "127.0.0.1:9090"; 44 | const maxMobileWidth = 600; 45 | const maxLaptopWidth = 840; 46 | const geodataLoaderMemconservative = "memconservative"; 47 | const geodataLoaderStandard = "standard"; 48 | const defaultTestUrl = "https://www.gstatic.com/generate_204"; 49 | final filter = ImageFilter.blur( 50 | sigmaX: 5, 51 | sigmaY: 5, 52 | tileMode: TileMode.mirror, 53 | ); 54 | 55 | const navigationItemListEquality = ListEquality(); 56 | const connectionListEquality = ListEquality(); 57 | const stringListEquality = ListEquality(); 58 | const logListEquality = ListEquality(); 59 | const groupListEquality = ListEquality(); 60 | const externalProviderListEquality = ListEquality(); 61 | const packageListEquality = ListEquality(); 62 | const hotKeyActionListEquality = ListEquality(); 63 | const stringAndStringMapEquality = MapEquality(); 64 | const stringAndStringMapEntryIterableEquality = 65 | IterableEquality>(); 66 | const stringAndIntQMapEquality = MapEquality(); 67 | const stringSetEquality = SetEquality(); 68 | const keyboardModifierListEquality = SetEquality(); 69 | 70 | const viewModeColumnsMap = { 71 | ViewMode.mobile: [2, 1], 72 | ViewMode.laptop: [3, 2], 73 | ViewMode.desktop: [4, 3], 74 | }; 75 | 76 | const defaultPrimaryColor = Colors.brown; 77 | -------------------------------------------------------------------------------- /lib/common/context.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/widgets/scaffold.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | extension BuildContextExtension on BuildContext { 5 | 6 | CommonScaffoldState? get commonScaffoldState { 7 | return findAncestorStateOfType(); 8 | } 9 | 10 | Size get appSize{ 11 | return MediaQuery.of(this).size; 12 | } 13 | 14 | double get viewWidth { 15 | return appSize.width; 16 | } 17 | 18 | ColorScheme get colorScheme => Theme.of(this).colorScheme; 19 | 20 | TextTheme get textTheme => Theme.of(this).textTheme; 21 | } 22 | -------------------------------------------------------------------------------- /lib/common/datetime.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/app_localizations.dart'; 2 | 3 | extension DateTimeExtension on DateTime { 4 | bool get isBeforeNow { 5 | return isBefore(DateTime.now()); 6 | } 7 | 8 | bool isBeforeSecure(DateTime? dateTime) { 9 | if (dateTime == null) { 10 | return false; 11 | } 12 | return true; 13 | } 14 | 15 | String get lastUpdateTimeDesc { 16 | final currentDateTime = DateTime.now(); 17 | final difference = currentDateTime.difference(this); 18 | final days = difference.inDays; 19 | if (days >= 365) { 20 | return "${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}"; 21 | } 22 | if (days >= 30) { 23 | return "${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}"; 24 | } 25 | if (days >= 1) { 26 | return "$days ${appLocalizations.days}${appLocalizations.ago}"; 27 | } 28 | final hours = difference.inHours; 29 | if (hours >= 1) { 30 | return "$hours ${appLocalizations.hours}${appLocalizations.ago}"; 31 | } 32 | final minutes = difference.inMinutes; 33 | if (minutes >= 1) { 34 | return "$minutes ${appLocalizations.minutes}${appLocalizations.ago}"; 35 | } 36 | return appLocalizations.just; 37 | } 38 | 39 | String get show { 40 | return toIso8601String().substring(0, 10); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/common/dav_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:fl_clash/common/common.dart'; 5 | import 'package:fl_clash/models/models.dart'; 6 | import 'package:webdav_client/webdav_client.dart'; 7 | 8 | class DAVClient { 9 | late Client client; 10 | Completer pingCompleter = Completer(); 11 | late String fileName; 12 | 13 | DAVClient(DAV dav) { 14 | client = newClient( 15 | dav.uri, 16 | user: dav.user, 17 | password: dav.password, 18 | ); 19 | fileName = dav.fileName; 20 | client.setHeaders( 21 | { 22 | 'accept-charset': 'utf-8', 23 | 'Content-Type': 'text/xml', 24 | }, 25 | ); 26 | client.setConnectTimeout(8000); 27 | client.setSendTimeout(8000); 28 | client.setReceiveTimeout(8000); 29 | pingCompleter.complete(_ping()); 30 | } 31 | 32 | Future _ping() async { 33 | try { 34 | await client.ping(); 35 | return true; 36 | } catch (_) { 37 | return false; 38 | } 39 | } 40 | 41 | get root => "/$appName"; 42 | 43 | get backupFile => "$root/$fileName"; 44 | 45 | backup(Uint8List data) async { 46 | await client.mkdir("$root"); 47 | await client.write("$backupFile", data); 48 | return true; 49 | } 50 | 51 | Future> recovery() async { 52 | await client.mkdir("$root"); 53 | final data = await client.read(backupFile); 54 | return data; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/common/function.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | class Debouncer { 4 | final Duration delay; 5 | Timer? _timer; 6 | 7 | Debouncer({required this.delay}); 8 | 9 | void call(Function action, List positionalArguments, [Map? namedArguments]) { 10 | _timer?.cancel(); 11 | _timer = Timer(delay, () => Function.apply(action, positionalArguments, namedArguments)); 12 | } 13 | } 14 | 15 | Function debounce(F func,{int milliseconds = 600}) { 16 | Timer? timer; 17 | 18 | return ([List? args, Map? namedArgs]) { 19 | if (timer != null) { 20 | timer!.cancel(); 21 | } 22 | timer = Timer(Duration(milliseconds: milliseconds), () async { 23 | await Function.apply(func, args ?? [], namedArgs); 24 | }); 25 | }; 26 | } -------------------------------------------------------------------------------- /lib/common/http.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | import '../state.dart'; 6 | 7 | class FlClashHttpOverrides extends HttpOverrides { 8 | @override 9 | HttpClient createHttpClient(SecurityContext? context) { 10 | final client = super.createHttpClient(context); 11 | client.badCertificateCallback = (_, __, ___) => true; 12 | client.findProxy = (url) { 13 | debugPrint("find $url"); 14 | final appController = globalState.appController; 15 | final port = appController.clashConfig.mixedPort; 16 | final isStart = appController.appFlowingState.isStart; 17 | if (!isStart) return "DIRECT"; 18 | return "PROXY localhost:$port"; 19 | }; 20 | return client; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/common/icons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class IconsExt{ 4 | static const IconData target = 5 | IconData(0xe900, fontFamily: "Icons"); 6 | } -------------------------------------------------------------------------------- /lib/common/iterable.dart: -------------------------------------------------------------------------------- 1 | extension IterableExt on Iterable { 2 | Iterable separated(T separator) sync* { 3 | final iterator = this.iterator; 4 | if (!iterator.moveNext()) return; 5 | 6 | yield iterator.current; 7 | 8 | while (iterator.moveNext()) { 9 | yield separator; 10 | yield iterator.current; 11 | } 12 | } 13 | 14 | Iterable> chunks(int size) sync* { 15 | if (length == 0) return; 16 | var iterator = this.iterator; 17 | while (iterator.moveNext()) { 18 | var chunk = [iterator.current]; 19 | for (var i = 1; i < size && iterator.moveNext(); i++) { 20 | chunk.add(iterator.current); 21 | } 22 | yield chunk; 23 | } 24 | } 25 | 26 | Iterable fill( 27 | int length, { 28 | required T Function(int count) filler, 29 | }) sync* { 30 | int count = 0; 31 | for (var item in this) { 32 | yield item; 33 | count++; 34 | if (count >= length) return; 35 | } 36 | while (count < length) { 37 | yield filler(count); 38 | count++; 39 | } 40 | } 41 | } 42 | 43 | extension DoubleListExt on List { 44 | int findInterval(num target) { 45 | if (isEmpty) return -1; 46 | if (target < first) return -1; 47 | if (target >= last) return length - 1; 48 | 49 | int left = 0; 50 | int right = length - 1; 51 | 52 | while (left <= right) { 53 | int mid = left + (right - left) ~/ 2; 54 | 55 | if (mid == length - 1 || 56 | (this[mid] <= target && target < this[mid + 1])) { 57 | return mid; 58 | } else if (target < this[mid]) { 59 | right = mid - 1; 60 | } else { 61 | left = mid + 1; 62 | } 63 | } 64 | 65 | return -1; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/common/launch.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:fl_clash/models/models.dart' hide Process; 4 | import 'package:launch_at_startup/launch_at_startup.dart'; 5 | 6 | import 'constant.dart'; 7 | import 'system.dart'; 8 | import 'windows.dart'; 9 | 10 | class AutoLaunch { 11 | static AutoLaunch? _instance; 12 | 13 | AutoLaunch._internal() { 14 | launchAtStartup.setup( 15 | appName: appName, 16 | appPath: Platform.resolvedExecutable, 17 | ); 18 | } 19 | 20 | factory AutoLaunch() { 21 | _instance ??= AutoLaunch._internal(); 22 | return _instance!; 23 | } 24 | 25 | Future get isEnable async { 26 | return await launchAtStartup.isEnabled(); 27 | } 28 | 29 | Future get windowsIsEnable async { 30 | final res = await Process.run( 31 | 'schtasks', 32 | ['/Query', '/TN', appName, '/V', "/FO", "LIST"], 33 | runInShell: true, 34 | ); 35 | return res.stdout.toString().contains(Platform.resolvedExecutable); 36 | } 37 | 38 | Future enable() async { 39 | if (Platform.isWindows) { 40 | await windowsDisable(); 41 | } 42 | return await launchAtStartup.enable(); 43 | } 44 | 45 | windowsDisable() async { 46 | final res = await Process.run( 47 | 'schtasks', 48 | [ 49 | '/Delete', 50 | '/TN', 51 | appName, 52 | '/F', 53 | ], 54 | runInShell: true, 55 | ); 56 | return res.exitCode == 0; 57 | } 58 | 59 | Future windowsEnable() async { 60 | await disable(); 61 | return await windows?.registerTask(appName) ?? false; 62 | } 63 | 64 | Future disable() async { 65 | return await launchAtStartup.disable(); 66 | } 67 | 68 | updateStatus(AutoLaunchState state) async { 69 | final isAdminAutoLaunch = state.isAdminAutoLaunch; 70 | final isAutoLaunch = state.isAutoLaunch; 71 | if (Platform.isWindows && isAdminAutoLaunch) { 72 | if (await windowsIsEnable == isAutoLaunch) return; 73 | if (isAutoLaunch) { 74 | final isEnable = await windowsEnable(); 75 | if (!isEnable) { 76 | enable(); 77 | } 78 | } else { 79 | windowsDisable(); 80 | } 81 | return; 82 | } 83 | if (await isEnable == isAutoLaunch) return; 84 | if (isAutoLaunch == true) { 85 | enable(); 86 | } else { 87 | disable(); 88 | } 89 | } 90 | } 91 | 92 | final autoLaunch = system.isDesktop ? AutoLaunch() : null; 93 | -------------------------------------------------------------------------------- /lib/common/link.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:app_links/app_links.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | 6 | typedef InstallConfigCallBack = void Function(String url); 7 | 8 | class LinkManager { 9 | static LinkManager? _instance; 10 | late AppLinks _appLinks; 11 | StreamSubscription? subscription; 12 | 13 | LinkManager._internal() { 14 | _appLinks = AppLinks(); 15 | } 16 | 17 | initAppLinksListen(installConfigCallBack) async { 18 | debugPrint("initAppLinksListen"); 19 | destroy(); 20 | subscription = _appLinks.allUriLinkStream.listen( 21 | (uri) { 22 | debugPrint('onAppLink: $uri'); 23 | if (uri.host == 'install-config') { 24 | final parameters = uri.queryParameters; 25 | final url = parameters['url']; 26 | if (url != null) { 27 | installConfigCallBack(url); 28 | } 29 | } 30 | }, 31 | ); 32 | } 33 | 34 | 35 | destroy(){ 36 | if (subscription != null) { 37 | subscription?.cancel(); 38 | subscription = null; 39 | } 40 | } 41 | 42 | factory LinkManager() { 43 | _instance ??= LinkManager._internal(); 44 | return _instance!; 45 | } 46 | } 47 | 48 | final linkManager = LinkManager(); 49 | -------------------------------------------------------------------------------- /lib/common/list.dart: -------------------------------------------------------------------------------- 1 | extension ListExtension on List { 2 | List intersection(List list) { 3 | return where((item) => list.contains(item)).toList(); 4 | } 5 | 6 | List> batch(int maxConcurrent) { 7 | final batches = (length / maxConcurrent).ceil(); 8 | final List> res = []; 9 | for (int i = 0; i < batches; i++) { 10 | if (i != batches - 1) { 11 | res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1))); 12 | } else { 13 | res.add(sublist(i * maxConcurrent, length)); 14 | } 15 | } 16 | return res; 17 | } 18 | 19 | List safeSublist(int start) { 20 | if(start <= 0) return this; 21 | if(start > length) return []; 22 | return sublist(start); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/common/measure.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/common.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class Measure { 6 | final TextScaler _textScale; 7 | late BuildContext context; 8 | 9 | Measure.of(this.context) 10 | : _textScale = TextScaler.linear( 11 | WidgetsBinding.instance.platformDispatcher.textScaleFactor); 12 | 13 | Size computeTextSize(Text text) { 14 | final textPainter = TextPainter( 15 | text: TextSpan(text: text.data, style: text.style), 16 | maxLines: text.maxLines, 17 | textScaler: _textScale, 18 | textDirection: text.textDirection ?? TextDirection.ltr, 19 | )..layout(); 20 | return textPainter.size; 21 | } 22 | 23 | double? _bodyMediumHeight; 24 | Size? _bodyLargeSize; 25 | double? _bodySmallHeight; 26 | double? _labelSmallHeight; 27 | double? _labelMediumHeight; 28 | double? _titleLargeHeight; 29 | double? _titleMediumHeight; 30 | 31 | double get bodyMediumHeight { 32 | _bodyMediumHeight ??= computeTextSize( 33 | Text( 34 | "X", 35 | style: context.textTheme.bodyMedium, 36 | ), 37 | ).height; 38 | return _bodyMediumHeight!; 39 | } 40 | 41 | 42 | Size get bodyLargeSize { 43 | _bodyLargeSize ??= computeTextSize( 44 | Text( 45 | "X", 46 | style: context.textTheme.bodyLarge, 47 | ), 48 | ); 49 | return _bodyLargeSize!; 50 | } 51 | 52 | double get bodySmallHeight { 53 | _bodySmallHeight ??= computeTextSize( 54 | Text( 55 | "X", 56 | style: context.textTheme.bodySmall, 57 | ), 58 | ).height; 59 | return _bodySmallHeight!; 60 | } 61 | 62 | double get labelSmallHeight { 63 | _labelSmallHeight ??= computeTextSize( 64 | Text( 65 | "X", 66 | style: context.textTheme.labelSmall, 67 | ), 68 | ).height; 69 | return _labelSmallHeight!; 70 | } 71 | 72 | double get labelMediumHeight { 73 | _labelMediumHeight ??= computeTextSize( 74 | Text( 75 | "X", 76 | style: context.textTheme.labelMedium, 77 | ), 78 | ).height; 79 | return _labelMediumHeight!; 80 | } 81 | 82 | double get titleLargeHeight { 83 | _titleLargeHeight ??= computeTextSize( 84 | Text( 85 | "X", 86 | style: context.textTheme.titleLarge, 87 | ), 88 | ).height; 89 | return _titleLargeHeight!; 90 | } 91 | 92 | double get titleMediumHeight { 93 | _titleMediumHeight ??= computeTextSize( 94 | Text( 95 | "X", 96 | style: context.textTheme.titleMedium, 97 | ), 98 | ).height; 99 | return _titleMediumHeight!; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/common/navigation.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/enum/enum.dart'; 2 | import 'package:fl_clash/fragments/fragments.dart'; 3 | import 'package:fl_clash/models/models.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class Navigation { 7 | static Navigation? _instance; 8 | 9 | getItems({ 10 | bool openLogs = false, 11 | bool hasProxies = false, 12 | }) { 13 | return [ 14 | const NavigationItem( 15 | icon: Icon(Icons.space_dashboard), 16 | label: "dashboard", 17 | fragment: DashboardFragment(), 18 | ), 19 | NavigationItem( 20 | icon: const Icon(Icons.rocket), 21 | label: "proxies", 22 | fragment: const ProxiesFragment(), 23 | modes: hasProxies 24 | ? [NavigationItemMode.mobile, NavigationItemMode.desktop] 25 | : [], 26 | ), 27 | const NavigationItem( 28 | icon: Icon(Icons.folder), 29 | label: "profiles", 30 | fragment: ProfilesFragment(), 31 | ), 32 | const NavigationItem( 33 | icon: Icon(Icons.view_timeline), 34 | label: "requests", 35 | fragment: RequestsFragment(), 36 | description: "requestsDesc", 37 | modes: [NavigationItemMode.desktop, NavigationItemMode.more], 38 | ), 39 | const NavigationItem( 40 | icon: Icon(Icons.ballot), 41 | label: "connections", 42 | fragment: ConnectionsFragment(), 43 | description: "connectionsDesc", 44 | modes: [NavigationItemMode.desktop, NavigationItemMode.more], 45 | ), 46 | const NavigationItem( 47 | icon: Icon(Icons.storage), 48 | label: "resources", 49 | description: "resourcesDesc", 50 | keep: false, 51 | fragment: Resources(), 52 | modes: [NavigationItemMode.desktop, NavigationItemMode.more], 53 | ), 54 | NavigationItem( 55 | icon: const Icon(Icons.adb), 56 | label: "logs", 57 | fragment: const LogsFragment(), 58 | description: "logsDesc", 59 | modes: openLogs 60 | ? [NavigationItemMode.desktop, NavigationItemMode.more] 61 | : [], 62 | ), 63 | const NavigationItem( 64 | icon: Icon(Icons.construction), 65 | label: "tools", 66 | fragment: ToolsFragment(), 67 | modes: [NavigationItemMode.desktop, NavigationItemMode.mobile], 68 | ), 69 | ]; 70 | } 71 | 72 | Navigation._internal(); 73 | 74 | factory Navigation() { 75 | _instance ??= Navigation._internal(); 76 | return _instance!; 77 | } 78 | } 79 | 80 | final navigation = Navigation(); 81 | -------------------------------------------------------------------------------- /lib/common/network.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | extension NetworkInterfaceExt on NetworkInterface { 4 | bool get isWifi { 5 | final nameLowCase = name.toLowerCase(); 6 | if (nameLowCase.contains('wlan') || 7 | nameLowCase.contains('wi-fi') || 8 | nameLowCase == 'en0' || 9 | nameLowCase == 'eth0') { 10 | return true; 11 | } 12 | 13 | return false; 14 | } 15 | 16 | bool get includesIPv4 { 17 | return addresses.any((addr) => addr.isIPv4); 18 | } 19 | } 20 | 21 | extension InternetAddressExt on InternetAddress { 22 | bool get isIPv4 { 23 | return type == InternetAddressType.IPv4; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/common/num.dart: -------------------------------------------------------------------------------- 1 | extension NumExtension on num { 2 | String fixed({digit = 2}) { 3 | return toStringAsFixed(truncateToDouble() == this ? 0 : digit); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/common/package.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:package_info_plus/package_info_plus.dart'; 4 | 5 | import 'common.dart'; 6 | 7 | extension PackageInfoExtension on PackageInfo { 8 | String get ua => [ 9 | "$appName/v$version", 10 | "clash-verge", 11 | "Platform/${Platform.operatingSystem}", 12 | ].join(" "); 13 | } 14 | -------------------------------------------------------------------------------- /lib/common/path.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:path/path.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | import 'constant.dart'; 8 | 9 | class AppPath { 10 | static AppPath? _instance; 11 | Completer dataDir = Completer(); 12 | Completer downloadDir = Completer(); 13 | Completer tempDir = Completer(); 14 | late String appDirPath; 15 | 16 | // Future _createDesktopCacheDir() async { 17 | // final dir = Directory(path); 18 | // if (await dir.exists()) { 19 | // await dir.create(recursive: true); 20 | // } 21 | // return dir; 22 | // } 23 | 24 | AppPath._internal() { 25 | appDirPath = join(dirname(Platform.resolvedExecutable)); 26 | getApplicationSupportDirectory().then((value) { 27 | dataDir.complete(value); 28 | }); 29 | getTemporaryDirectory().then((value){ 30 | tempDir.complete(value); 31 | }); 32 | getDownloadsDirectory().then((value) { 33 | downloadDir.complete(value); 34 | }); 35 | // if (Platform.isAndroid) { 36 | // getApplicationSupportDirectory().then((value) { 37 | // cacheDir.complete(value); 38 | // }); 39 | // } else { 40 | // _createDesktopCacheDir().then((value) { 41 | // cacheDir.complete(value); 42 | // }); 43 | // } 44 | } 45 | 46 | factory AppPath() { 47 | _instance ??= AppPath._internal(); 48 | return _instance!; 49 | } 50 | 51 | Future getDownloadDirPath() async { 52 | final directory = await downloadDir.future; 53 | return directory.path; 54 | } 55 | 56 | Future getHomeDirPath() async { 57 | final directory = await dataDir.future; 58 | return directory.path; 59 | } 60 | 61 | Future getProfilesPath() async { 62 | final directory = await dataDir.future; 63 | return join(directory.path, profilesDirectoryName); 64 | } 65 | 66 | Future getProfilePath(String? id) async { 67 | if (id == null) return null; 68 | final directory = await getProfilesPath(); 69 | return join(directory, "$id.yaml"); 70 | } 71 | 72 | Future get tempPath async { 73 | final directory = await tempDir.future; 74 | return directory.path; 75 | } 76 | } 77 | 78 | final appPath = AppPath(); 79 | -------------------------------------------------------------------------------- /lib/common/picker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:file_picker/file_picker.dart'; 5 | import 'package:fl_clash/common/common.dart'; 6 | import 'package:image_picker/image_picker.dart'; 7 | 8 | class Picker { 9 | Future pickerFile() async { 10 | final filePickerResult = await FilePicker.platform.pickFiles( 11 | withData: true, 12 | allowMultiple: false, 13 | initialDirectory: await appPath.getDownloadDirPath(), 14 | ); 15 | return filePickerResult?.files.first; 16 | } 17 | 18 | Future saveFile(String fileName, Uint8List bytes) async { 19 | final path = await FilePicker.platform.saveFile( 20 | fileName: fileName, 21 | initialDirectory: await appPath.getDownloadDirPath(), 22 | bytes: Platform.isAndroid ? bytes : null, 23 | ); 24 | if (!Platform.isAndroid && path != null) { 25 | final file = await File(path).create(recursive: true); 26 | await file.writeAsBytes(bytes); 27 | } 28 | return path; 29 | } 30 | 31 | Future pickerConfigQRCode() async { 32 | final xFile = await ImagePicker().pickImage(source: ImageSource.gallery); 33 | final bytes = await xFile?.readAsBytes(); 34 | if (bytes == null) return null; 35 | final result = await other.parseQRCode(bytes); 36 | if (result == null || !result.isUrl) { 37 | throw appLocalizations.pleaseUploadValidQrcode; 38 | } 39 | return result; 40 | } 41 | } 42 | 43 | final picker = Picker(); 44 | -------------------------------------------------------------------------------- /lib/common/preferences.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | import '../models/models.dart'; 8 | import 'constant.dart'; 9 | 10 | class Preferences { 11 | static Preferences? _instance; 12 | Completer sharedPreferencesCompleter = Completer(); 13 | 14 | Preferences._internal() { 15 | SharedPreferences.getInstance() 16 | .then((value) => sharedPreferencesCompleter.complete(value)); 17 | } 18 | 19 | factory Preferences() { 20 | _instance ??= Preferences._internal(); 21 | return _instance!; 22 | } 23 | 24 | Future getClashConfig() async { 25 | final preferences = await sharedPreferencesCompleter.future; 26 | final clashConfigString = preferences.getString(clashConfigKey); 27 | if (clashConfigString == null) return null; 28 | final clashConfigMap = json.decode(clashConfigString); 29 | try { 30 | return ClashConfig.fromJson(clashConfigMap); 31 | } catch (e) { 32 | debugPrint(e.toString()); 33 | return null; 34 | } 35 | } 36 | 37 | Future saveClashConfig(ClashConfig clashConfig) async { 38 | final preferences = await sharedPreferencesCompleter.future; 39 | return preferences.setString( 40 | clashConfigKey, 41 | json.encode(clashConfig), 42 | ); 43 | } 44 | 45 | Future getConfig() async { 46 | final preferences = await sharedPreferencesCompleter.future; 47 | final configString = preferences.getString(configKey); 48 | if (configString == null) return null; 49 | final configMap = json.decode(configString); 50 | try { 51 | return Config.fromJson(configMap); 52 | } catch (e) { 53 | debugPrint(e.toString()); 54 | return null; 55 | } 56 | } 57 | 58 | Future saveConfig(Config config) async { 59 | final preferences = await sharedPreferencesCompleter.future; 60 | return preferences.setString( 61 | configKey, 62 | json.encode(config), 63 | ); 64 | } 65 | 66 | clearPreferences() async { 67 | final sharedPreferencesIns = await sharedPreferencesCompleter.future; 68 | sharedPreferencesIns.clear(); 69 | } 70 | } 71 | 72 | final preferences = Preferences(); -------------------------------------------------------------------------------- /lib/common/protocol.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:win32_registry/win32_registry.dart'; 4 | 5 | class Protocol { 6 | static Protocol? _instance; 7 | 8 | Protocol._internal(); 9 | 10 | factory Protocol() { 11 | _instance ??= Protocol._internal(); 12 | return _instance!; 13 | } 14 | 15 | void register(String scheme) { 16 | String protocolRegKey = 'Software\\Classes\\$scheme'; 17 | RegistryValue protocolRegValue = const RegistryValue( 18 | 'URL Protocol', 19 | RegistryValueType.string, 20 | '', 21 | ); 22 | String protocolCmdRegKey = 'shell\\open\\command'; 23 | RegistryValue protocolCmdRegValue = RegistryValue( 24 | '', 25 | RegistryValueType.string, 26 | '"${Platform.resolvedExecutable}" "%1"', 27 | ); 28 | final regKey = Registry.currentUser.createKey(protocolRegKey); 29 | regKey.createValue(protocolRegValue); 30 | regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue); 31 | } 32 | } 33 | 34 | final protocol = Protocol(); -------------------------------------------------------------------------------- /lib/common/proxy.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/system.dart'; 2 | import 'package:proxy/proxy.dart'; 3 | 4 | final proxy = system.isDesktop ? Proxy() : null; 5 | -------------------------------------------------------------------------------- /lib/common/scroll.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:fl_clash/common/common.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class BaseScrollBehavior extends MaterialScrollBehavior { 7 | @override 8 | Set get dragDevices => { 9 | PointerDeviceKind.touch, 10 | PointerDeviceKind.stylus, 11 | PointerDeviceKind.invertedStylus, 12 | PointerDeviceKind.trackpad, 13 | if (system.isDesktop) PointerDeviceKind.mouse, 14 | PointerDeviceKind.unknown, 15 | }; 16 | } 17 | 18 | class HiddenBarScrollBehavior extends BaseScrollBehavior { 19 | @override 20 | Widget buildScrollbar( 21 | BuildContext context, 22 | Widget child, 23 | ScrollableDetails details, 24 | ) { 25 | return child; 26 | } 27 | } 28 | 29 | class ShowBarScrollBehavior extends BaseScrollBehavior { 30 | @override 31 | Widget buildScrollbar( 32 | BuildContext context, 33 | Widget child, 34 | ScrollableDetails details, 35 | ) { 36 | return Scrollbar( 37 | interactive: true, 38 | controller: details.controller, 39 | child: child, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/common/string.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | extension StringExtension on String { 7 | bool get isUrl { 8 | return RegExp(r'^(http|https|ftp)://').hasMatch(this); 9 | } 10 | 11 | int compareToLower(String other) { 12 | return toLowerCase().compareTo( 13 | other.toLowerCase(), 14 | ); 15 | } 16 | 17 | List get encodeUtf16LeWithBom { 18 | final byteData = ByteData(length * 2); 19 | final bom = [0xFF, 0xFE]; 20 | for (int i = 0; i < length; i++) { 21 | int charCode = codeUnitAt(i); 22 | byteData.setUint16(i * 2, charCode, Endian.little); 23 | } 24 | return bom + byteData.buffer.asUint8List(); 25 | } 26 | 27 | Uint8List? get getBase64 { 28 | final regExp = RegExp(r'base64,(.*)'); 29 | final match = regExp.firstMatch(this); 30 | final realValue = match?.group(1) ?? ''; 31 | if (realValue.isEmpty) { 32 | return null; 33 | } 34 | try { 35 | return base64.decode(realValue); 36 | } catch (e) { 37 | return null; 38 | } 39 | } 40 | 41 | bool get isRegex { 42 | try { 43 | RegExp(this); 44 | return true; 45 | } catch (e) { 46 | debugPrint(e.toString()); 47 | return false; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/common/system.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:device_info_plus/device_info_plus.dart'; 4 | import 'package:fl_clash/plugins/app.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | import 'window.dart'; 8 | 9 | class System { 10 | static System? _instance; 11 | 12 | System._internal(); 13 | 14 | factory System() { 15 | _instance ??= System._internal(); 16 | return _instance!; 17 | } 18 | 19 | bool get isDesktop => 20 | Platform.isWindows || Platform.isMacOS || Platform.isLinux; 21 | 22 | get isAdmin async { 23 | if (!Platform.isWindows) return false; 24 | final result = await Process.run('net', ['session'], runInShell: true); 25 | return result.exitCode == 0; 26 | } 27 | 28 | Future get version async { 29 | final deviceInfo = await DeviceInfoPlugin().deviceInfo; 30 | return switch (Platform.operatingSystem) { 31 | "macos" => (deviceInfo as MacOsDeviceInfo).majorVersion, 32 | "android" => (deviceInfo as AndroidDeviceInfo).version.sdkInt, 33 | "windows" => (deviceInfo as WindowsDeviceInfo).majorVersion, 34 | String() => 0 35 | }; 36 | } 37 | 38 | back() async { 39 | await app?.moveTaskToBack(); 40 | await window?.hide(); 41 | } 42 | 43 | exit() async { 44 | if (Platform.isAndroid) { 45 | await SystemNavigator.pop(); 46 | } 47 | await window?.close(); 48 | } 49 | } 50 | 51 | final system = System(); 52 | -------------------------------------------------------------------------------- /lib/common/text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'color.dart'; 3 | 4 | extension TextStyleExtension on TextStyle { 5 | TextStyle get toLight => copyWith(color: color?.toLight()); 6 | 7 | TextStyle get toLighter => copyWith(color: color?.toLighter()); 8 | 9 | TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500); 10 | 11 | TextStyle get toBold => copyWith(fontWeight: FontWeight.bold); 12 | 13 | TextStyle get toMinus => copyWith(fontSize: fontSize! - 2); 14 | } 15 | -------------------------------------------------------------------------------- /lib/common/window.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:fl_clash/models/config.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:window_manager/window_manager.dart'; 6 | import 'package:windows_single_instance/windows_single_instance.dart'; 7 | 8 | import 'protocol.dart'; 9 | import 'system.dart'; 10 | 11 | class Window { 12 | init(WindowProps props, int version) async { 13 | if (Platform.isWindows) { 14 | await WindowsSingleInstance.ensureSingleInstance([], "FlClash"); 15 | protocol.register("clash"); 16 | protocol.register("clashmeta"); 17 | protocol.register("flclash"); 18 | } 19 | await windowManager.ensureInitialized(); 20 | WindowOptions windowOptions = WindowOptions( 21 | size: Size(props.width, props.height), 22 | minimumSize: const Size(380, 500), 23 | ); 24 | if (props.left != null || props.top != null) { 25 | await windowManager.setPosition( 26 | Offset(props.left ?? 0, props.top ?? 0), 27 | ); 28 | } else { 29 | await windowManager.setAlignment(Alignment.center); 30 | } 31 | if(!Platform.isMacOS || version > 10){ 32 | await windowManager.setTitleBarStyle(TitleBarStyle.hidden); 33 | } 34 | await windowManager.waitUntilReadyToShow(windowOptions, () async { 35 | await windowManager.setPreventClose(true); 36 | }); 37 | } 38 | 39 | show() async { 40 | await windowManager.show(); 41 | await windowManager.focus(); 42 | await windowManager.setSkipTaskbar(false); 43 | } 44 | 45 | Future isVisible() async { 46 | return await windowManager.isVisible(); 47 | } 48 | 49 | close() async { 50 | exit(0); 51 | } 52 | 53 | hide() async { 54 | await windowManager.hide(); 55 | await windowManager.setSkipTaskbar(true); 56 | } 57 | } 58 | 59 | final window = system.isDesktop ? Window() : null; 60 | -------------------------------------------------------------------------------- /lib/fragments/config/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/common.dart'; 2 | import 'package:fl_clash/fragments/config/dns.dart'; 3 | import 'package:fl_clash/fragments/config/general.dart'; 4 | import 'package:fl_clash/fragments/config/network.dart'; 5 | import 'package:fl_clash/widgets/widgets.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class ConfigFragment extends StatefulWidget { 9 | const ConfigFragment({super.key}); 10 | 11 | @override 12 | State createState() => _ConfigFragmentState(); 13 | } 14 | 15 | class _ConfigFragmentState extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | List items = [ 19 | ListItem.open( 20 | title: Text(appLocalizations.network), 21 | subtitle: Text(appLocalizations.networkDesc), 22 | leading: const Icon(Icons.vpn_key), 23 | delegate: OpenDelegate( 24 | title: appLocalizations.network, 25 | isScaffold: true, 26 | isBlur: false, 27 | widget: const NetworkListView(), 28 | ), 29 | ), 30 | ListItem.open( 31 | title: Text(appLocalizations.general), 32 | subtitle: Text(appLocalizations.generalDesc), 33 | leading: const Icon(Icons.build), 34 | delegate: OpenDelegate( 35 | title: appLocalizations.general, 36 | widget: generateListView( 37 | generalItems, 38 | ), 39 | isBlur: false, 40 | extendPageWidth: 360, 41 | ), 42 | ), 43 | ListItem.open( 44 | title: const Text("DNS"), 45 | subtitle: Text(appLocalizations.dnsDesc), 46 | leading: const Icon(Icons.dns), 47 | delegate: const OpenDelegate( 48 | title: "DNS", 49 | widget: DnsListView(), 50 | isScaffold: true, 51 | isBlur: false, 52 | extendPageWidth: 360, 53 | ), 54 | ) 55 | ]; 56 | return generateListView( 57 | items 58 | .separated( 59 | const Divider( 60 | height: 0, 61 | ), 62 | ) 63 | .toList(), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/fragments/dashboard/core_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/models/models.dart'; 2 | import 'package:fl_clash/widgets/widgets.dart'; 3 | import 'package:fl_clash/common/common.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class CoreInfo extends StatelessWidget { 8 | const CoreInfo({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Selector( 13 | selector: (_, appState) => appState.versionInfo, 14 | builder: (_, versionInfo, __) { 15 | return CommonCard( 16 | onPressed: () {}, 17 | info: Info( 18 | label: appLocalizations.coreInfo, 19 | iconData: Icons.memory, 20 | ), 21 | child: Container( 22 | alignment: Alignment.centerLeft, 23 | padding: const EdgeInsets.all(16).copyWith(top: 0), 24 | child: Column( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | mainAxisSize: MainAxisSize.min, 27 | children: [ 28 | Flexible( 29 | flex: 1, 30 | child: Text( 31 | versionInfo?.clashName ?? '', 32 | style: context 33 | .textTheme 34 | .titleMedium 35 | ?.toSoftBold, 36 | ), 37 | ), 38 | const SizedBox( 39 | height: 8, 40 | ), 41 | Flexible( 42 | flex: 1, 43 | child: Text( 44 | versionInfo?.version ?? '', 45 | style: context 46 | .textTheme 47 | .titleLarge 48 | ?.toSoftBold, 49 | ), 50 | ), 51 | ], 52 | ), 53 | ), 54 | ); 55 | }, 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/fragments/dashboard/outbound_mode.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/common.dart'; 2 | import 'package:fl_clash/enum/enum.dart'; 3 | import 'package:fl_clash/models/models.dart'; 4 | import 'package:fl_clash/state.dart'; 5 | import 'package:fl_clash/widgets/widgets.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:intl/intl.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class OutboundMode extends StatelessWidget { 11 | const OutboundMode({super.key}); 12 | 13 | _changeMode(BuildContext context, Mode? value) async { 14 | final appController = globalState.appController; 15 | final clashConfig = appController.clashConfig; 16 | if (value == null || clashConfig.mode == value) return; 17 | clashConfig.mode = value; 18 | appController.addCheckIpNumDebounce(); 19 | } 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Selector( 24 | selector: (_, clashConfig) => clashConfig.mode, 25 | builder: (_, mode, __) { 26 | return CommonCard( 27 | onPressed: () {}, 28 | info: Info( 29 | label: appLocalizations.outboundMode, 30 | iconData: Icons.call_split_sharp, 31 | ), 32 | child: Padding( 33 | padding: const EdgeInsets.only(bottom: 16), 34 | child: Column( 35 | mainAxisSize: MainAxisSize.min, 36 | crossAxisAlignment: CrossAxisAlignment.start, 37 | mainAxisAlignment: MainAxisAlignment.start, 38 | children: [ 39 | for (final item in Mode.values) 40 | ListItem.radio( 41 | horizontalTitleGap: 4, 42 | prue: true, 43 | padding: const EdgeInsets.only( 44 | left: 12, 45 | right: 16, 46 | top: 8, 47 | bottom: 8, 48 | ), 49 | delegate: RadioDelegate( 50 | value: item, 51 | groupValue: mode, 52 | onChanged: (value) async { 53 | _changeMode(context, value); 54 | }, 55 | ), 56 | title: Text( 57 | Intl.message(item.name), 58 | style: 59 | Theme.of(context).textTheme.titleMedium?.toSoftBold, 60 | ), 61 | ), 62 | ], 63 | ), 64 | ), 65 | ); 66 | }, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/fragments/dashboard/traffic_usage.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/common.dart'; 2 | import 'package:fl_clash/models/models.dart'; 3 | import 'package:fl_clash/widgets/widgets.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class TrafficUsage extends StatelessWidget { 8 | const TrafficUsage({super.key}); 9 | 10 | Widget getTrafficDataItem( 11 | BuildContext context, 12 | IconData iconData, 13 | TrafficValue trafficValue, 14 | ) { 15 | return Row( 16 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 17 | mainAxisSize: MainAxisSize.max, 18 | children: [ 19 | Flexible( 20 | flex: 1, 21 | child: Row( 22 | mainAxisSize: MainAxisSize.max, 23 | mainAxisAlignment: MainAxisAlignment.start, 24 | children: [ 25 | Icon( 26 | iconData, 27 | size: 18, 28 | ), 29 | const SizedBox( 30 | width: 8, 31 | ), 32 | Flexible( 33 | flex: 1, 34 | child: Text( 35 | trafficValue.showValue, 36 | style: context.textTheme.labelLarge?.copyWith(fontSize: 18), 37 | maxLines: 1, 38 | ), 39 | ), 40 | ], 41 | ), 42 | ), 43 | Text( 44 | trafficValue.showUnit, 45 | style: context.textTheme.labelMedium?.toLight, 46 | ), 47 | ], 48 | ); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return CommonCard( 54 | onPressed: () {}, 55 | info: Info( 56 | label: appLocalizations.trafficUsage, 57 | iconData: Icons.data_saver_off, 58 | ), 59 | child: Selector( 60 | selector: (_, appFlowingState) => appFlowingState.totalTraffic, 61 | builder: (_, totalTraffic, __) { 62 | final upTotalTrafficValue = totalTraffic.up; 63 | final downTotalTrafficValue = totalTraffic.down; 64 | return Padding( 65 | padding: const EdgeInsets.all(16).copyWith(top: 0), 66 | child: Column( 67 | mainAxisSize: MainAxisSize.min, 68 | children: [ 69 | Flexible( 70 | flex: 1, 71 | child: getTrafficDataItem( 72 | context, 73 | Icons.arrow_upward, 74 | upTotalTrafficValue, 75 | ), 76 | ), 77 | const SizedBox( 78 | height: 4, 79 | ), 80 | Flexible( 81 | flex: 1, 82 | child: getTrafficDataItem( 83 | context, 84 | Icons.arrow_downward, 85 | downTotalTrafficValue, 86 | ), 87 | ), 88 | ], 89 | ), 90 | ); 91 | }, 92 | ), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/fragments/fragments.dart: -------------------------------------------------------------------------------- 1 | export 'proxies/proxies.dart'; 2 | export 'dashboard/dashboard.dart'; 3 | export 'tools.dart'; 4 | export 'profiles/profiles.dart'; 5 | export 'logs.dart'; 6 | export 'connections.dart'; 7 | export 'access.dart'; 8 | export 'config/config.dart'; 9 | export 'application_setting.dart'; 10 | export 'about.dart'; 11 | export 'backup_and_recovery.dart'; 12 | export 'resources.dart'; 13 | export 'requests.dart'; 14 | -------------------------------------------------------------------------------- /lib/l10n/intl/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'dart:async'; 13 | 14 | import 'package:flutter/foundation.dart'; 15 | import 'package:intl/intl.dart'; 16 | import 'package:intl/message_lookup_by_library.dart'; 17 | import 'package:intl/src/intl_helpers.dart'; 18 | 19 | import 'messages_en.dart' as messages_en; 20 | import 'messages_zh_CN.dart' as messages_zh_cn; 21 | 22 | typedef Future LibraryLoader(); 23 | Map _deferredLibraries = { 24 | 'en': () => new SynchronousFuture(null), 25 | 'zh_CN': () => new SynchronousFuture(null), 26 | }; 27 | 28 | MessageLookupByLibrary? _findExact(String localeName) { 29 | switch (localeName) { 30 | case 'en': 31 | return messages_en.messages; 32 | case 'zh_CN': 33 | return messages_zh_cn.messages; 34 | default: 35 | return null; 36 | } 37 | } 38 | 39 | /// User programs should call this before using [localeName] for messages. 40 | Future initializeMessages(String localeName) { 41 | var availableLocale = Intl.verifiedLocale( 42 | localeName, (locale) => _deferredLibraries[locale] != null, 43 | onFailure: (_) => null); 44 | if (availableLocale == null) { 45 | return new SynchronousFuture(false); 46 | } 47 | var lib = _deferredLibraries[availableLocale]; 48 | lib == null ? new SynchronousFuture(false) : lib(); 49 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 50 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 51 | return new SynchronousFuture(true); 52 | } 53 | 54 | bool _messagesExistFor(String locale) { 55 | try { 56 | return _findExact(locale) != null; 57 | } catch (e) { 58 | return false; 59 | } 60 | } 61 | 62 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 63 | var actualLocale = 64 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 65 | if (actualLocale == null) return null; 66 | return _findExact(actualLocale); 67 | } 68 | -------------------------------------------------------------------------------- /lib/manager/android_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/models/models.dart'; 2 | import 'package:fl_clash/plugins/app.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class AndroidManager extends StatefulWidget { 8 | final Widget child; 9 | 10 | const AndroidManager({ 11 | super.key, 12 | required this.child, 13 | }); 14 | 15 | @override 16 | State createState() => _AndroidContainerState(); 17 | } 18 | 19 | class _AndroidContainerState extends State { 20 | @override 21 | void initState() { 22 | super.initState(); 23 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); 24 | } 25 | 26 | Widget _excludeContainer(Widget child) { 27 | return Selector( 28 | selector: (_, config) => config.appSetting.hidden, 29 | builder: (_, hidden, child) { 30 | app?.updateExcludeFromRecents(hidden); 31 | return child!; 32 | }, 33 | child: child, 34 | ); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return _excludeContainer(widget.child); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/manager/app_state_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/common.dart'; 2 | import 'package:fl_clash/models/models.dart'; 3 | import 'package:fl_clash/state.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class AppStateManager extends StatefulWidget { 8 | final Widget child; 9 | 10 | const AppStateManager({ 11 | super.key, 12 | required this.child, 13 | }); 14 | 15 | @override 16 | State createState() => _AppStateManagerState(); 17 | } 18 | 19 | class _AppStateManagerState extends State 20 | with WidgetsBindingObserver { 21 | 22 | _updateNavigationsContainer(Widget child) { 23 | return Selector2( 24 | selector: (_, appState, config) { 25 | final group = appState.currentGroups; 26 | final hasProfile = config.profiles.isNotEmpty; 27 | return UpdateNavigationsSelector( 28 | openLogs: config.appSetting.openLogs, 29 | hasProxies: group.isNotEmpty && hasProfile, 30 | ); 31 | }, 32 | builder: (context, state, child) { 33 | WidgetsBinding.instance.addPostFrameCallback( 34 | (_) { 35 | globalState.appController.appState.navigationItems = 36 | navigation.getItems( 37 | openLogs: state.openLogs, 38 | hasProxies: state.hasProxies, 39 | ); 40 | }, 41 | ); 42 | return child!; 43 | }, 44 | child: child, 45 | ); 46 | } 47 | 48 | @override 49 | void initState() { 50 | super.initState(); 51 | WidgetsBinding.instance.addObserver(this); 52 | } 53 | 54 | @override 55 | void dispose() { 56 | WidgetsBinding.instance.removeObserver(this); 57 | super.dispose(); 58 | } 59 | 60 | @override 61 | Future didChangeAppLifecycleState(AppLifecycleState state) async { 62 | final isPaused = state == AppLifecycleState.paused; 63 | if (isPaused) { 64 | await globalState.appController.savePreferences(); 65 | } 66 | } 67 | 68 | @override 69 | void didChangePlatformBrightness() { 70 | globalState.appController.appState.brightness = 71 | WidgetsBinding.instance.platformDispatcher.platformBrightness; 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return _updateNavigationsContainer( 77 | widget.child, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/manager/hotkey_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/common.dart'; 2 | import 'package:fl_clash/enum/enum.dart'; 3 | import 'package:fl_clash/models/common.dart'; 4 | import 'package:fl_clash/models/config.dart'; 5 | import 'package:fl_clash/state.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | import 'package:flutter/services.dart'; 8 | import 'package:hotkey_manager/hotkey_manager.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | class HotKeyManager extends StatelessWidget { 12 | final Widget child; 13 | 14 | const HotKeyManager({ 15 | super.key, 16 | required this.child, 17 | }); 18 | 19 | _handleHotKeyAction(HotAction action) async { 20 | switch (action) { 21 | case HotAction.mode: 22 | globalState.appController.updateMode(); 23 | case HotAction.start: 24 | globalState.appController.updateStart(); 25 | case HotAction.view: 26 | globalState.appController.updateVisible(); 27 | case HotAction.proxy: 28 | globalState.appController.updateSystemProxy(); 29 | case HotAction.tun: 30 | globalState.appController.updateTun(); 31 | } 32 | } 33 | 34 | _updateHotKeys({ 35 | required List hotKeyActions, 36 | }) async { 37 | await hotKeyManager.unregisterAll(); 38 | final hotkeyActionHandles = hotKeyActions.where( 39 | (hotKeyAction) { 40 | return hotKeyAction.key != null && hotKeyAction.modifiers.isNotEmpty; 41 | }, 42 | ).map( 43 | (hotKeyAction) async { 44 | final modifiers = hotKeyAction.modifiers 45 | .map((item) => item.toHotKeyModifier()) 46 | .toList(); 47 | final hotKey = HotKey( 48 | key: PhysicalKeyboardKey(hotKeyAction.key!), 49 | modifiers: modifiers, 50 | ); 51 | return await hotKeyManager.register( 52 | hotKey, 53 | keyDownHandler: (_) { 54 | _handleHotKeyAction(hotKeyAction.action); 55 | }, 56 | ); 57 | }, 58 | ); 59 | await Future.wait(hotkeyActionHandles); 60 | } 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | return Selector>( 65 | selector: (_, config) => config.hotKeyActions, 66 | shouldRebuild: (prev, next) { 67 | return !hotKeyActionListEquality.equals(prev, next); 68 | }, 69 | builder: (_, hotKeyActions, __) { 70 | _updateHotKeys(hotKeyActions: hotKeyActions); 71 | return child; 72 | }, 73 | child: child, 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/manager/manager.dart: -------------------------------------------------------------------------------- 1 | export 'tray_manager.dart'; 2 | export 'window_manager.dart'; 3 | export 'android_manager.dart'; 4 | export 'clash_manager.dart'; 5 | export 'tile_manager.dart'; 6 | export 'app_state_manager.dart'; 7 | export 'vpn_manager.dart'; 8 | export 'media_manager.dart'; 9 | export 'proxy_manager.dart'; -------------------------------------------------------------------------------- /lib/manager/media_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/common.dart'; 2 | import 'package:fl_clash/state.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class MediaManager extends StatelessWidget { 6 | final Widget child; 7 | 8 | const MediaManager({ 9 | super.key, 10 | required this.child, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | globalState.measure = Measure.of(context); 16 | return child; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/manager/proxy_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/proxy.dart'; 2 | import 'package:fl_clash/models/models.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class ProxyManager extends StatelessWidget { 7 | final Widget child; 8 | 9 | const ProxyManager({super.key, required this.child}); 10 | 11 | _updateProxy(ProxyState proxyState) { 12 | final isStart = proxyState.isStart; 13 | final systemProxy = proxyState.systemProxy; 14 | final port = proxyState.port; 15 | if (isStart && systemProxy) { 16 | proxy?.startProxy(port); 17 | }else{ 18 | proxy?.stopProxy(); 19 | } 20 | } 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Selector3( 25 | selector: (_, appFlowingState, config, clashConfig) => ProxyState( 26 | isStart: appFlowingState.isStart, 27 | systemProxy: config.desktopProps.systemProxy, 28 | port: clashConfig.mixedPort, 29 | ), 30 | builder: (_, state, child) { 31 | _updateProxy(state); 32 | return child!; 33 | }, 34 | child: child, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/manager/tile_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/plugins/tile.dart'; 2 | import 'package:fl_clash/state.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class TileManager extends StatefulWidget { 6 | final Widget child; 7 | 8 | const TileManager({ 9 | super.key, 10 | required this.child, 11 | }); 12 | 13 | @override 14 | State createState() => _TileContainerState(); 15 | } 16 | 17 | class _TileContainerState extends State with TileListener { 18 | 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return widget.child; 23 | } 24 | 25 | @override 26 | void onStart() { 27 | globalState.appController.updateStatus(true); 28 | super.onStart(); 29 | } 30 | 31 | @override 32 | void onStop() { 33 | globalState.appController.updateStatus(false); 34 | super.onStop(); 35 | } 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | tile?.addListener(this); 41 | } 42 | 43 | @override 44 | void dispose() { 45 | tile?.removeListener(this); 46 | super.dispose(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/manager/vpn_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/app_localizations.dart'; 2 | import 'package:fl_clash/models/models.dart'; 3 | import 'package:fl_clash/state.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | import '../../common/function.dart'; 8 | 9 | class VpnManager extends StatefulWidget { 10 | final Widget child; 11 | 12 | const VpnManager({ 13 | super.key, 14 | required this.child, 15 | }); 16 | 17 | @override 18 | State createState() => _VpnContainerState(); 19 | } 20 | 21 | class _VpnContainerState extends State { 22 | Function? vpnTipDebounce; 23 | 24 | showTip() { 25 | vpnTipDebounce ??= debounce(() async { 26 | WidgetsBinding.instance.addPostFrameCallback((_) { 27 | final appFlowingState = globalState.appController.appFlowingState; 28 | if (appFlowingState.isStart) { 29 | globalState.showSnackBar( 30 | context, 31 | message: appLocalizations.vpnTip, 32 | ); 33 | } 34 | }); 35 | }); 36 | vpnTipDebounce!(); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return Selector2( 42 | selector: (_, config, clashConfig) => VPNState( 43 | accessControl: config.accessControl, 44 | vpnProps: config.vpnProps, 45 | stack: clashConfig.tun.stack, 46 | ), 47 | shouldRebuild: (prev, next) { 48 | if (prev != next) { 49 | showTip(); 50 | } 51 | return prev != next; 52 | }, 53 | builder: (_, __, child) { 54 | return child!; 55 | }, 56 | child: widget.child, 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/models/generated/profile.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of '../profile.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$UserInfoImpl _$$UserInfoImplFromJson(Map json) => 10 | _$UserInfoImpl( 11 | upload: (json['upload'] as num?)?.toInt() ?? 0, 12 | download: (json['download'] as num?)?.toInt() ?? 0, 13 | total: (json['total'] as num?)?.toInt() ?? 0, 14 | expire: (json['expire'] as num?)?.toInt() ?? 0, 15 | ); 16 | 17 | Map _$$UserInfoImplToJson(_$UserInfoImpl instance) => 18 | { 19 | 'upload': instance.upload, 20 | 'download': instance.download, 21 | 'total': instance.total, 22 | 'expire': instance.expire, 23 | }; 24 | 25 | _$ProfileImpl _$$ProfileImplFromJson(Map json) => 26 | _$ProfileImpl( 27 | id: json['id'] as String, 28 | label: json['label'] as String?, 29 | currentGroupName: json['currentGroupName'] as String?, 30 | url: json['url'] as String? ?? "", 31 | lastUpdateDate: json['lastUpdateDate'] == null 32 | ? null 33 | : DateTime.parse(json['lastUpdateDate'] as String), 34 | autoUpdateDuration: 35 | Duration(microseconds: (json['autoUpdateDuration'] as num).toInt()), 36 | userInfo: json['userInfo'] == null 37 | ? null 38 | : UserInfo.fromJson(json['userInfo'] as Map), 39 | autoUpdate: json['autoUpdate'] as bool? ?? true, 40 | selectedMap: (json['selectedMap'] as Map?)?.map( 41 | (k, e) => MapEntry(k, e as String), 42 | ) ?? 43 | const {}, 44 | unfoldSet: (json['unfoldSet'] as List?) 45 | ?.map((e) => e as String) 46 | .toSet() ?? 47 | const {}, 48 | ); 49 | 50 | Map _$$ProfileImplToJson(_$ProfileImpl instance) => 51 | { 52 | 'id': instance.id, 53 | 'label': instance.label, 54 | 'currentGroupName': instance.currentGroupName, 55 | 'url': instance.url, 56 | 'lastUpdateDate': instance.lastUpdateDate?.toIso8601String(), 57 | 'autoUpdateDuration': instance.autoUpdateDuration.inMicroseconds, 58 | 'userInfo': instance.userInfo, 59 | 'autoUpdate': instance.autoUpdate, 60 | 'selectedMap': instance.selectedMap, 61 | 'unfoldSet': instance.unfoldSet.toList(), 62 | }; 63 | -------------------------------------------------------------------------------- /lib/models/models.dart: -------------------------------------------------------------------------------- 1 | export 'app.dart'; 2 | export 'clash_config.dart'; 3 | export 'config.dart'; 4 | export 'profile.dart'; 5 | export 'ffi.dart'; 6 | export 'selector.dart'; 7 | export 'common.dart'; -------------------------------------------------------------------------------- /lib/pages/pages.dart: -------------------------------------------------------------------------------- 1 | export 'home.dart'; 2 | export 'scan.dart'; -------------------------------------------------------------------------------- /lib/plugins/app.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | import 'dart:isolate'; 5 | 6 | import 'package:fl_clash/models/models.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter/services.dart'; 9 | 10 | class App { 11 | static App? _instance; 12 | late MethodChannel methodChannel; 13 | Function()? onExit; 14 | 15 | App._internal() { 16 | methodChannel = const MethodChannel("app"); 17 | methodChannel.setMethodCallHandler((call) async { 18 | switch (call.method) { 19 | case "exit": 20 | if (onExit != null) { 21 | await onExit!(); 22 | } 23 | default: 24 | throw MissingPluginException(); 25 | } 26 | }); 27 | } 28 | 29 | factory App() { 30 | _instance ??= App._internal(); 31 | return _instance!; 32 | } 33 | 34 | Future moveTaskToBack() async { 35 | return await methodChannel.invokeMethod("moveTaskToBack"); 36 | } 37 | 38 | Future> getPackages() async { 39 | final packagesString = 40 | await methodChannel.invokeMethod("getPackages"); 41 | return Isolate.run>(() { 42 | final List packagesRaw = 43 | packagesString != null ? json.decode(packagesString) : []; 44 | return packagesRaw.map((e) => Package.fromJson(e)).toList(); 45 | }); 46 | } 47 | 48 | Future> getChinaPackageNames() async { 49 | final packageNamesString = 50 | await methodChannel.invokeMethod("getChinaPackageNames"); 51 | return Isolate.run>(() { 52 | final List packageNamesRaw = 53 | packageNamesString != null ? json.decode(packageNamesString) : []; 54 | return packageNamesRaw.map((e) => e.toString()).toList(); 55 | }); 56 | } 57 | 58 | Future openFile(String path) async { 59 | return await methodChannel.invokeMethod("openFile", { 60 | "path": path, 61 | }) ?? 62 | false; 63 | } 64 | 65 | Future getPackageIcon(String packageName) async { 66 | final base64 = await methodChannel.invokeMethod("getPackageIcon", { 67 | "packageName": packageName, 68 | }); 69 | if (base64 == null) { 70 | return null; 71 | } 72 | return MemoryImage(base64Decode(base64)); 73 | } 74 | 75 | Future tip(String? message) async { 76 | return await methodChannel.invokeMethod("tip", { 77 | "message": "$message", 78 | }); 79 | } 80 | 81 | Future updateExcludeFromRecents(bool value) async { 82 | return await methodChannel.invokeMethod("updateExcludeFromRecents", { 83 | "value": value, 84 | }); 85 | } 86 | } 87 | 88 | final app = Platform.isAndroid ? App() : null; 89 | -------------------------------------------------------------------------------- /lib/plugins/service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'dart:isolate'; 4 | import 'package:flutter/services.dart'; 5 | 6 | class Service { 7 | static Service? _instance; 8 | late MethodChannel methodChannel; 9 | ReceivePort? receiver; 10 | 11 | Service._internal() { 12 | methodChannel = const MethodChannel("service"); 13 | } 14 | 15 | factory Service() { 16 | _instance ??= Service._internal(); 17 | return _instance!; 18 | } 19 | 20 | Future init() async { 21 | return await methodChannel.invokeMethod("init"); 22 | } 23 | 24 | Future destroy() async { 25 | return await methodChannel.invokeMethod("destroy"); 26 | } 27 | } 28 | 29 | final service = 30 | Platform.isAndroid ? Service() : null; 31 | -------------------------------------------------------------------------------- /lib/plugins/tile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | abstract mixin class TileListener { 8 | void onStart() {} 9 | 10 | void onStop() {} 11 | 12 | void onDetached(){ 13 | 14 | } 15 | } 16 | 17 | class Tile { 18 | StreamSubscription? subscription; 19 | 20 | final MethodChannel _channel = const MethodChannel('tile'); 21 | 22 | Tile._() { 23 | _channel.setMethodCallHandler(_methodCallHandler); 24 | } 25 | 26 | static final Tile instance = Tile._(); 27 | 28 | final ObserverList _listeners = ObserverList(); 29 | 30 | Future _methodCallHandler(MethodCall call) async { 31 | for (final TileListener listener in _listeners) { 32 | switch (call.method) { 33 | case "start": 34 | listener.onStart(); 35 | break; 36 | case "stop": 37 | listener.onStop(); 38 | break; 39 | case "detached": 40 | listener.onDetached(); 41 | break; 42 | } 43 | } 44 | } 45 | 46 | bool get hasListeners { 47 | return _listeners.isNotEmpty; 48 | } 49 | 50 | void addListener(TileListener listener) { 51 | _listeners.add(listener); 52 | } 53 | 54 | void removeListener(TileListener listener) { 55 | _listeners.remove(listener); 56 | } 57 | } 58 | 59 | final tile = Platform.isAndroid ? Tile.instance : null; 60 | -------------------------------------------------------------------------------- /lib/router/fade_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FadePage extends Page { 4 | final Widget child; 5 | final bool maintainState; 6 | final bool fullscreenDialog; 7 | final bool allowSnapshotting; 8 | 9 | const FadePage({ 10 | required this.child, 11 | this.maintainState = true, 12 | this.fullscreenDialog = false, 13 | this.allowSnapshotting = true, 14 | super.key, 15 | super.name, 16 | super.arguments, 17 | super.restorationId, 18 | }); 19 | 20 | @override 21 | Route createRoute(BuildContext context) { 22 | return FadePageRoute(page: this); 23 | } 24 | } 25 | 26 | class FadePageRoute extends PageRoute { 27 | final FadePage page; 28 | 29 | FadePageRoute({ 30 | required this.page, 31 | }) : super(settings: page); 32 | 33 | FadePage get _page => settings as FadePage; 34 | 35 | @override 36 | Widget buildPage(BuildContext context, Animation animation, 37 | Animation secondaryAnimation) { 38 | return _page.child; 39 | } 40 | 41 | @override 42 | Widget buildTransitions(BuildContext context, Animation animation, 43 | Animation secondaryAnimation, Widget child) { 44 | return FadeTransition( 45 | opacity: animation, 46 | child: child, 47 | ); 48 | } 49 | 50 | @override 51 | Duration get transitionDuration => const Duration(milliseconds: 600); 52 | 53 | @override 54 | bool get maintainState => false; 55 | 56 | @override 57 | Color? get barrierColor => null; 58 | 59 | @override 60 | String? get barrierLabel => null; 61 | } 62 | -------------------------------------------------------------------------------- /lib/widgets/animate_grid.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | typedef AnimatedGridBuilder = Widget Function(BuildContext, T item); 4 | 5 | class AnimateGrid extends StatelessWidget { 6 | final int columns; 7 | final double itemHeight; 8 | final double gap; 9 | final List items; 10 | final Key Function(T item) keyBuilder; 11 | final AnimatedGridBuilder builder; 12 | final Duration duration; 13 | final Curve curve; 14 | 15 | const AnimateGrid({ 16 | super.key, 17 | required this.items, 18 | required this.itemHeight, 19 | required this.keyBuilder, 20 | required this.builder, 21 | this.gap = 8, 22 | this.duration = const Duration(milliseconds: 300), 23 | this.curve = Curves.easeOut, 24 | this.columns = 2, 25 | }); 26 | 27 | int _rows(int columns, int count) => (count / columns).ceil(); 28 | 29 | Offset _getOffset( 30 | int index, 31 | int count, 32 | double itemWidth, 33 | double itemHeight, 34 | ) { 35 | final xIndex = index % columns; 36 | final yIndex = (index / columns).floor(); 37 | return Offset( 38 | xIndex * itemWidth + xIndex * gap, yIndex * itemHeight + yIndex * gap); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return LayoutBuilder(builder: (_, constraints) { 44 | assert(constraints.hasBoundedHeight == false); 45 | final gapWidth = (columns - 1) * gap; 46 | final width = constraints.maxWidth; 47 | final itemWidth = (width - gapWidth) / columns; 48 | final count = items.length; 49 | final rows = _rows(columns, count); 50 | final gapHeight = (rows - 1) * gap; 51 | final height = rows * itemHeight + gapHeight; 52 | return SizedBox( 53 | width: width, 54 | height: height, 55 | child: Stack( 56 | children: [ 57 | for (var i = 0; i <= count - 1; i++) 58 | Builder( 59 | key: keyBuilder(items[i]), 60 | builder: (context) { 61 | final item = items[i]; 62 | final offset = _getOffset( 63 | i, 64 | count, 65 | itemWidth, 66 | itemHeight, 67 | ); 68 | return TweenAnimationBuilder( 69 | tween: Tween(end: offset), 70 | duration: duration, 71 | curve: curve, 72 | builder: (_, offset, child) { 73 | return Transform.translate( 74 | offset: offset, 75 | child: child, 76 | ); 77 | }, 78 | child: SizedBox( 79 | height: itemHeight, 80 | width: itemWidth, 81 | child: builder( 82 | context, 83 | item, 84 | ), 85 | ), 86 | ); 87 | }, 88 | ), 89 | ], 90 | ), 91 | ); 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/widgets/back_scope.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | 4 | import 'package:fl_clash/state.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | class BackScope extends StatefulWidget { 8 | final Widget child; 9 | 10 | const BackScope({super.key, required this.child}); 11 | 12 | @override 13 | State createState() => _PopContainerState(); 14 | } 15 | 16 | class _PopContainerState extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | if (Platform.isAndroid) { 20 | return PopScope( 21 | canPop: false, 22 | onPopInvoked: (_) async { 23 | final canPop = Navigator.canPop(context); 24 | if (canPop) { 25 | Navigator.pop(context); 26 | } else { 27 | await globalState.appController.handleBackOrExit(); 28 | } 29 | }, 30 | child: widget.child, 31 | ); 32 | } 33 | return widget.child; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/widgets/builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/models/models.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | class ScrollOverBuilder extends StatefulWidget { 6 | final Widget Function(bool isOver) builder; 7 | 8 | const ScrollOverBuilder({ 9 | super.key, 10 | required this.builder, 11 | }); 12 | 13 | @override 14 | State createState() => _ScrollOverBuilderState(); 15 | } 16 | 17 | class _ScrollOverBuilderState extends State { 18 | final isOverNotifier = ValueNotifier(false); 19 | 20 | @override 21 | void dispose() { 22 | super.dispose(); 23 | isOverNotifier.dispose(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return NotificationListener( 29 | onNotification: (scrollNotification) { 30 | isOverNotifier.value = scrollNotification.metrics.maxScrollExtent > 0; 31 | return true; 32 | }, 33 | child: ValueListenableBuilder( 34 | valueListenable: isOverNotifier, 35 | builder: (_, isOver, __) { 36 | return widget.builder(isOver); 37 | }, 38 | ), 39 | ); 40 | } 41 | } 42 | 43 | class ProxiesActionsBuilder extends StatelessWidget { 44 | final Widget? child; 45 | final Widget Function( 46 | ProxiesActionsState state, 47 | Widget? child, 48 | ) builder; 49 | 50 | const ProxiesActionsBuilder({ 51 | super.key, 52 | required this.child, 53 | required this.builder, 54 | }); 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return Selector( 59 | selector: (_, appState) => ProxiesActionsState( 60 | isCurrent: appState.currentLabel == "proxies", 61 | hasProvider: appState.providers.isNotEmpty, 62 | ), 63 | builder: (_, state, child) => builder(state, child), 64 | child: child, 65 | ); 66 | } 67 | } 68 | 69 | typedef StateWidgetBuilder = Widget Function(T state); 70 | 71 | class LocaleBuilder extends StatelessWidget { 72 | final StateWidgetBuilder builder; 73 | 74 | const LocaleBuilder({ 75 | super.key, 76 | required this.builder, 77 | }); 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | return Selector( 82 | selector: (_, config) => config.appSetting.locale, 83 | builder: (_, state, __) { 84 | return builder(state); 85 | }, 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/widgets/chip.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/enum/enum.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class CommonChip extends StatelessWidget { 5 | final String label; 6 | final VoidCallback? onPressed; 7 | final ChipType type; 8 | final Widget? avatar; 9 | 10 | const CommonChip({ 11 | super.key, 12 | required this.label, 13 | this.onPressed, 14 | this.avatar, 15 | this.type = ChipType.action, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | if (type == ChipType.delete) { 21 | return Chip( 22 | avatar: avatar, 23 | labelPadding:const EdgeInsets.symmetric( 24 | vertical: 0, 25 | horizontal: 4, 26 | ), 27 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 28 | onDeleted: onPressed ?? () {}, 29 | side: 30 | BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)), 31 | labelStyle: Theme.of(context).textTheme.bodyMedium, 32 | label: Text(label), 33 | ); 34 | } 35 | return ActionChip( 36 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 37 | avatar: avatar, 38 | labelPadding:const EdgeInsets.symmetric( 39 | vertical: 0, 40 | horizontal: 4, 41 | ), 42 | onPressed: onPressed ?? () {}, 43 | side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)), 44 | labelStyle: Theme.of(context).textTheme.bodyMedium, 45 | label: Text(label), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/widgets/disabled_mask.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DisabledMask extends StatefulWidget { 4 | final Widget child; 5 | final bool status; 6 | 7 | const DisabledMask({ 8 | super.key, 9 | required this.child, 10 | this.status = true, 11 | }); 12 | 13 | @override 14 | State createState() => _DisabledMaskState(); 15 | } 16 | 17 | class _DisabledMaskState extends State { 18 | GlobalKey childKey = GlobalKey(); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final child = Container( 23 | key: childKey, 24 | child: widget.child, 25 | ); 26 | if (!widget.status) { 27 | return child; 28 | } 29 | return ColorFiltered( 30 | colorFilter: const ColorFilter.matrix([ 31 | 0.2126, 32 | 0.7152, 33 | 0.0722, 34 | 0, 35 | 30, 36 | 0.2126, 37 | 0.7152, 38 | 0.0722, 39 | 0, 40 | 30, 41 | 0.2126, 42 | 0.7152, 43 | 0.0722, 44 | 0, 45 | 30, 46 | 0, 47 | 0, 48 | 0, 49 | 1, 50 | 0, 51 | ]), 52 | child: child, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/widgets/fade_box.dart: -------------------------------------------------------------------------------- 1 | import 'package:animations/animations.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class FadeBox extends StatelessWidget { 5 | final Widget child; 6 | 7 | const FadeBox({ 8 | super.key, 9 | required this.child, 10 | }); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return PageTransitionSwitcher( 15 | transitionBuilder: ( 16 | child, 17 | animation, 18 | secondaryAnimation, 19 | ) { 20 | return Container( 21 | alignment: Alignment.centerLeft, 22 | child: FadeThroughTransition( 23 | animation: animation, 24 | fillColor: Colors.transparent, 25 | secondaryAnimation: secondaryAnimation, 26 | child: child, 27 | ), 28 | ); 29 | }, 30 | child: child, 31 | ); 32 | } 33 | } 34 | 35 | class FadeScaleBox extends StatelessWidget { 36 | final Widget child; 37 | 38 | const FadeScaleBox({ 39 | super.key, 40 | required this.child, 41 | }); 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return PageTransitionSwitcher( 46 | transitionBuilder: ( 47 | child, 48 | animation, 49 | secondaryAnimation, 50 | ) { 51 | return FadeScaleTransition( 52 | animation: animation, 53 | child: child, 54 | ); 55 | }, 56 | child: child, 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/widgets/float_layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FloatLayout extends StatelessWidget { 4 | final Widget floatingWidget; 5 | 6 | final Widget child; 7 | 8 | const FloatLayout({ 9 | super.key, 10 | required this.floatingWidget, 11 | required this.child, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Stack( 17 | fit: StackFit.loose, 18 | children: [ 19 | Center( 20 | child: child, 21 | ), 22 | Positioned( 23 | bottom: 0, 24 | right: 0, 25 | child: Container( 26 | child: floatingWidget, 27 | ), 28 | ), 29 | ], 30 | ); 31 | } 32 | } 33 | 34 | class FloatWrapper extends StatelessWidget { 35 | final Widget child; 36 | 37 | const FloatWrapper({ 38 | super.key, 39 | required this.child, 40 | }); 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return Container( 45 | margin: const EdgeInsets.all(kFloatingActionButtonMargin), 46 | child: child, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/widgets/icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:fl_clash/common/common.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class CommonIcon extends StatelessWidget { 6 | final String src; 7 | final double size; 8 | 9 | const CommonIcon({ 10 | super.key, 11 | required this.src, 12 | required this.size, 13 | }); 14 | 15 | Widget _defaultIcon() { 16 | return Icon( 17 | IconsExt.target, 18 | size: size, 19 | ); 20 | } 21 | 22 | Widget _buildIcon() { 23 | if (src.isEmpty) { 24 | return _defaultIcon(); 25 | } 26 | final base64 = src.getBase64; 27 | if (base64 != null) { 28 | return Image.memory( 29 | base64, 30 | gaplessPlayback: true, 31 | errorBuilder: (_, error, ___) { 32 | return _defaultIcon(); 33 | }, 34 | ); 35 | } 36 | return CachedNetworkImage( 37 | imageUrl: src, 38 | fadeInDuration: Duration.zero, 39 | fadeOutDuration: Duration.zero, 40 | errorWidget: (_, __, ___) => _defaultIcon(), 41 | ); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return SizedBox( 47 | width: size, 48 | height: size, 49 | child: _buildIcon(), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/widgets/keep_scope.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class KeepScope extends StatefulWidget { 4 | final Widget child; 5 | final bool keep; 6 | 7 | const KeepScope({ 8 | super.key, 9 | required this.child, 10 | this.keep = true, 11 | }); 12 | 13 | @override 14 | State createState() => _KeepContainerState(); 15 | } 16 | 17 | class _KeepContainerState extends State 18 | with AutomaticKeepAliveClientMixin { 19 | @override 20 | Widget build(BuildContext context) { 21 | super.build(context); 22 | return widget.child; 23 | } 24 | 25 | @override 26 | bool get wantKeepAlive => widget.keep; 27 | } 28 | -------------------------------------------------------------------------------- /lib/widgets/null_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../common/common.dart'; 3 | 4 | class NullStatus extends StatelessWidget { 5 | final String label; 6 | 7 | const NullStatus({super.key, required this.label}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Center( 12 | child: Text( 13 | label, 14 | style: Theme.of(context).textTheme.titleMedium?.toBold, 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/widgets/setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/common.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'card.dart'; 5 | 6 | class SettingInfoCard extends StatelessWidget { 7 | final Info info; 8 | final bool? isSelected; 9 | final VoidCallback onPressed; 10 | 11 | const SettingInfoCard( 12 | this.info, { 13 | super.key, 14 | this.isSelected, 15 | required this.onPressed, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return CommonCard( 21 | isSelected: isSelected, 22 | onPressed: onPressed, 23 | child: Padding( 24 | padding: const EdgeInsets.all(12), 25 | child: Row( 26 | mainAxisSize: MainAxisSize.min, 27 | mainAxisAlignment: MainAxisAlignment.start, 28 | children: [ 29 | Flexible( 30 | child: Icon(info.iconData), 31 | ), 32 | const SizedBox( 33 | width: 8, 34 | ), 35 | Flexible( 36 | child: Text( 37 | info.label, 38 | style: context.textTheme.bodyMedium, 39 | ), 40 | ), 41 | ], 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | 48 | class SettingTextCard extends StatelessWidget { 49 | final String text; 50 | final bool? isSelected; 51 | final VoidCallback onPressed; 52 | 53 | const SettingTextCard( 54 | this.text, { 55 | super.key, 56 | this.isSelected, 57 | required this.onPressed, 58 | }); 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | return CommonCard( 63 | onPressed: onPressed, 64 | isSelected: isSelected, 65 | child: Padding( 66 | padding: const EdgeInsets.all(12), 67 | child: Text( 68 | text, 69 | style: context.textTheme.bodyMedium, 70 | ), 71 | ), 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/widgets/sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_clash/common/common.dart'; 2 | import 'package:fl_clash/enum/enum.dart'; 3 | import 'package:fl_clash/state.dart'; 4 | import 'package:fl_clash/widgets/scaffold.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'side_sheet.dart'; 7 | 8 | showExtendPage( 9 | BuildContext context, { 10 | required Widget body, 11 | required String title, 12 | double? extendPageWidth, 13 | bool isScaffold = false, 14 | bool isBlur = true, 15 | Widget? action, 16 | }) { 17 | final NavigatorState navigator = Navigator.of(context); 18 | final globalKey = GlobalKey(); 19 | final uniqueBody = Container( 20 | key: globalKey, 21 | child: body, 22 | ); 23 | final isMobile = 24 | globalState.appController.appState.viewMode == ViewMode.mobile; 25 | if (isMobile) { 26 | Navigator.of(context).push( 27 | MaterialPageRoute( 28 | builder: (_) => CommonScaffold( 29 | title: title, 30 | body: uniqueBody, 31 | ), 32 | ), 33 | ); 34 | return; 35 | } 36 | final isNotSide = isMobile || isScaffold; 37 | navigator.push( 38 | ModalSideSheetRoute( 39 | modalBarrierColor: Colors.black38, 40 | builder: (context) { 41 | final commonScaffold = CommonScaffold( 42 | automaticallyImplyLeading: isNotSide, 43 | actions: isNotSide 44 | ? null 45 | : [ 46 | const SizedBox( 47 | height: kToolbarHeight, 48 | width: kToolbarHeight, 49 | child: CloseButton(), 50 | ), 51 | ], 52 | title: title, 53 | body: uniqueBody, 54 | ); 55 | return SizedBox( 56 | width: isMobile ? context.viewWidth : extendPageWidth ?? 300, 57 | child: commonScaffold, 58 | ); 59 | }, 60 | constraints: const BoxConstraints(), 61 | filter: isBlur ? filter : null, 62 | ), 63 | ); 64 | } 65 | 66 | showSheet({ 67 | required BuildContext context, 68 | required WidgetBuilder builder, 69 | required String title, 70 | bool isScrollControlled = true, 71 | double width = 320, 72 | }) { 73 | final viewMode = globalState.appController.appState.viewMode; 74 | final isMobile = viewMode == ViewMode.mobile; 75 | if (isMobile) { 76 | showModalBottomSheet( 77 | context: context, 78 | isScrollControlled: isScrollControlled, 79 | builder: (context) { 80 | return SafeArea( 81 | child: builder( 82 | context, 83 | ), 84 | ); 85 | }, 86 | showDragHandle: true, 87 | useSafeArea: true, 88 | ); 89 | } else { 90 | showModalSideSheet( 91 | useSafeArea: true, 92 | isScrollControlled: isScrollControlled, 93 | context: context, 94 | constraints: BoxConstraints( 95 | maxWidth: width, 96 | ), 97 | body: SafeArea( 98 | child: builder(context), 99 | ), 100 | title: title, 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/widgets/text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:emoji_regex/emoji_regex.dart'; 3 | 4 | import '../state.dart'; 5 | 6 | class TooltipText extends StatelessWidget { 7 | final Text text; 8 | 9 | const TooltipText({ 10 | super.key, 11 | required this.text, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return LayoutBuilder( 17 | builder: (context, container) { 18 | final maxWidth = container.maxWidth; 19 | final size = globalState.measure.computeTextSize( 20 | text, 21 | ); 22 | if (maxWidth < size.width) { 23 | return Tooltip( 24 | preferBelow: false, 25 | message: text.data, 26 | child: text, 27 | ); 28 | } 29 | return text; 30 | }, 31 | ); 32 | } 33 | } 34 | 35 | class EmojiText extends StatelessWidget { 36 | final String text; 37 | final TextStyle? style; 38 | final int? maxLines; 39 | final TextOverflow? overflow; 40 | 41 | const EmojiText( 42 | this.text, { 43 | super.key, 44 | this.maxLines, 45 | this.overflow, 46 | this.style, 47 | }); 48 | 49 | List _buildTextSpans(String emojis) { 50 | final List spans = []; 51 | final matches = emojiRegex().allMatches(text); 52 | 53 | int lastMatchEnd = 0; 54 | for (final match in matches) { 55 | if (match.start > lastMatchEnd) { 56 | spans.add( 57 | TextSpan( 58 | text: text.substring(lastMatchEnd, match.start), style: style), 59 | ); 60 | } 61 | spans.add( 62 | TextSpan( 63 | text:match.group(0), 64 | style: style?.copyWith( 65 | fontFamily: "Twemoji", 66 | ), 67 | ), 68 | ); 69 | lastMatchEnd = match.end; 70 | } 71 | if (lastMatchEnd < text.length) { 72 | spans.add( 73 | TextSpan( 74 | text: text.substring(lastMatchEnd), 75 | style: style, 76 | ), 77 | ); 78 | } 79 | 80 | return spans; 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return RichText( 86 | maxLines: maxLines, 87 | overflow: overflow ?? TextOverflow.clip, 88 | text: TextSpan( 89 | children: _buildTextSpans(text), 90 | ), 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'scaffold.dart'; 2 | export 'float_layout.dart'; 3 | export 'popup_menu.dart'; 4 | export 'card.dart'; 5 | export 'list.dart'; 6 | export 'line_chart.dart'; 7 | export 'grid.dart'; 8 | export 'open_container.dart'; 9 | export 'color_scheme_box.dart'; 10 | export 'null_status.dart'; 11 | export 'disabled_mask.dart'; 12 | export 'side_sheet.dart'; 13 | export 'sheet.dart'; 14 | export 'animate_grid.dart'; 15 | export 'chip.dart'; 16 | export 'fade_box.dart'; 17 | export 'text.dart'; 18 | export 'connection_item.dart'; 19 | export 'builder.dart'; 20 | export 'setting.dart'; 21 | export 'input.dart'; 22 | export 'keep_scope.dart'; 23 | export 'back_scope.dart'; 24 | export 'icon.dart'; 25 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /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 | #include 17 | 18 | void fl_register_plugins(FlPluginRegistry* registry) { 19 | g_autoptr(FlPluginRegistrar) dynamic_color_registrar = 20 | fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); 21 | dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); 22 | g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = 23 | fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); 24 | file_selector_plugin_register_with_registrar(file_selector_linux_registrar); 25 | g_autoptr(FlPluginRegistrar) gtk_registrar = 26 | fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); 27 | gtk_plugin_register_with_registrar(gtk_registrar); 28 | g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar = 29 | fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin"); 30 | hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar); 31 | g_autoptr(FlPluginRegistrar) screen_retriever_registrar = 32 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); 33 | screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); 34 | g_autoptr(FlPluginRegistrar) tray_manager_registrar = 35 | fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); 36 | tray_manager_plugin_register_with_registrar(tray_manager_registrar); 37 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 38 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 39 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 40 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 41 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 42 | window_manager_plugin_register_with_registrar(window_manager_registrar); 43 | } 44 | -------------------------------------------------------------------------------- /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 | dynamic_color 7 | file_selector_linux 8 | gtk 9 | hotkey_manager_linux 10 | screen_retriever 11 | tray_manager 12 | url_launcher_linux 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}/linux 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}/linux plugins/${ffi_plugin}) 30 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 31 | endforeach(ffi_plugin) 32 | -------------------------------------------------------------------------------- /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.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: FlClash 2 | 3 | icon: ./assets/images/icon.png 4 | 5 | keywords: 6 | - FlClash 7 | - Clash 8 | - ClashMeta 9 | - Proxy 10 | 11 | generic_name: FlClash 12 | 13 | 14 | categories: 15 | - Network 16 | 17 | startup_notify: true 18 | 19 | include: [] -------------------------------------------------------------------------------- /linux/packaging/deb/make_config.yaml: -------------------------------------------------------------------------------- 1 | display_name: FlClash 2 | package_name: FlClash 3 | maintainer: 4 | name: chen08209 5 | email: chen08209@gmail.com 6 | 7 | priority: optional 8 | section: x11 9 | installed_size: 6604 10 | essential: false 11 | icon: ./assets/images/icon.png 12 | 13 | 14 | keywords: 15 | - FlClash 16 | - Clash 17 | - ClashMeta 18 | - Proxy 19 | 20 | generic_name: FlClash 21 | 22 | categories: 23 | - Network 24 | 25 | startup_notify: true -------------------------------------------------------------------------------- /linux/packaging/rpm/make_config.yaml: -------------------------------------------------------------------------------- 1 | display_name: FlClash 2 | 3 | packager: chen08209 4 | packagerEmail: chen08209@gmail.com 5 | license: Other 6 | 7 | priority: optional 8 | section: x11 9 | installed_size: 6604 10 | essential: false 11 | icon: ./assets/images/icon.png 12 | 13 | keywords: 14 | - FlClash 15 | - Clash 16 | - ClashMeta 17 | - Proxy 18 | 19 | generic_name: FlClash 20 | 21 | group: Applications/Internet 22 | 23 | startup_notify: true -------------------------------------------------------------------------------- /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? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import app_links 9 | import device_info_plus 10 | import dynamic_color 11 | import file_selector_macos 12 | import hotkey_manager_macos 13 | import mobile_scanner 14 | import package_info_plus 15 | import path_provider_foundation 16 | import screen_retriever 17 | import shared_preferences_foundation 18 | import sqflite 19 | import tray_manager 20 | import url_launcher_macos 21 | import window_manager 22 | 23 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 24 | AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) 25 | DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) 26 | DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) 27 | FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 28 | HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) 29 | MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) 30 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 31 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 32 | ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) 33 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 34 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 35 | TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) 36 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 37 | WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) 38 | } 39 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | 7 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 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/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 = FlClash 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.follow. 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 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLName 27 | 28 | CFBundleURLSchemes 29 | 30 | clash 31 | clashmeta 32 | flclash 33 | 34 | 35 | 36 | CFBundleVersion 37 | $(FLUTTER_BUILD_NUMBER) 38 | LSMinimumSystemVersion 39 | $(MACOSX_DEPLOYMENT_TARGET) 40 | NSHumanReadableCopyright 41 | $(PRODUCT_COPYRIGHT) 42 | NSMainNibFile 43 | MainMenu 44 | NSPrincipalClass 45 | NSApplication 46 | 47 | 48 | -------------------------------------------------------------------------------- /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() 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 | override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { 17 | super.order(place, relativeTo: otherWin) 18 | hiddenWindowAtLaunch() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 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 | -------------------------------------------------------------------------------- /macos/packaging/dmg/make_config.yaml: -------------------------------------------------------------------------------- 1 | title: FlClash 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: FlClash.app 11 | -------------------------------------------------------------------------------- /plugins/proxy/.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 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /plugins/proxy/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "e1e47221e86272429674bec4f1bd36acc4fc7b77" 8 | channel: "stable" 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 17 | base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 18 | - platform: android 19 | create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 20 | base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 21 | - platform: windows 22 | create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 23 | base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /plugins/proxy/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /plugins/proxy/LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /plugins/proxy/README.md: -------------------------------------------------------------------------------- 1 | # proxy 2 | 3 | A new Flutter plugin project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter 8 | [plug-in package](https://flutter.dev/developing-packages/), 9 | a specialized package that includes platform-specific implementation code for 10 | Android and/or iOS. 11 | 12 | For help getting started with Flutter development, view the 13 | [online documentation](https://flutter.dev/docs), which offers tutorials, 14 | samples, guidance on mobile development, and a full API reference. 15 | 16 | -------------------------------------------------------------------------------- /plugins/proxy/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /plugins/proxy/lib/proxy_method_channel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | import 'proxy_platform_interface.dart'; 5 | 6 | /// An implementation of [ProxyPlatform] that uses method channels. 7 | class MethodChannelProxy extends ProxyPlatform { 8 | /// The method channel used to interact with the native platform. 9 | @visibleForTesting 10 | final methodChannel = const MethodChannel('proxy'); 11 | 12 | MethodChannelProxy(); 13 | 14 | @override 15 | Future startProxy(int port) async { 16 | return await methodChannel.invokeMethod("StartProxy", {'port': port}); 17 | } 18 | 19 | @override 20 | Future stopProxy() async { 21 | return await methodChannel.invokeMethod("StopProxy"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugins/proxy/lib/proxy_platform_interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 2 | 3 | import 'proxy_method_channel.dart'; 4 | 5 | abstract class ProxyPlatform extends PlatformInterface { 6 | /// Constructs a ProxyPlatform. 7 | ProxyPlatform() : super(token: _token); 8 | 9 | static final Object _token = Object(); 10 | 11 | static ProxyPlatform _instance = MethodChannelProxy(); 12 | 13 | /// The default instance of [ProxyPlatform] to use. 14 | /// 15 | /// Defaults to [MethodChannelProxy]. 16 | static ProxyPlatform get instance => _instance; 17 | 18 | static set instance(ProxyPlatform instance) { 19 | PlatformInterface.verifyToken(instance, _token); 20 | _instance = instance; 21 | } 22 | 23 | Future startProxy(int port) { 24 | throw UnimplementedError('startProxy() has not been implemented.'); 25 | } 26 | 27 | Future stopProxy() { 28 | throw UnimplementedError('stopProxy() has not been implemented.'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /plugins/proxy/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: proxy 2 | description: A new Flutter plugin project. 3 | version: 0.0.1 4 | homepage: 5 | 6 | environment: 7 | sdk: '>=3.1.0 <4.0.0' 8 | flutter: '>=3.3.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | plugin_platform_interface: ^2.0.2 14 | path: ^1.8.3 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | flutter_lints: ^2.0.0 20 | 21 | # For information on the generic Dart part of this file, see the 22 | # following page: https://dart.dev/tools/pub/pubspec 23 | 24 | # The following section is specific to Flutter packages. 25 | flutter: 26 | # This section identifies this Flutter project as a plugin project. 27 | # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) 28 | # which should be registered in the plugin registry. This is required for 29 | # using method channels. 30 | # The Android 'package' specifies package in which the registered class is. 31 | # This is required for using method channels on Android. 32 | # The 'ffiPlugin' specifies that native code should be built and bundled. 33 | # This is required for using `dart:ffi`. 34 | # All these are used by the tooling to maintain consistency when 35 | # adding or updating assets for this project. 36 | plugin: 37 | platforms: 38 | windows: 39 | pluginClass: ProxyPluginCApi 40 | 41 | # To add assets to your plugin package, add an assets section, like this: 42 | # assets: 43 | # - images/a_dot_burr.jpeg 44 | # - images/a_dot_ham.jpeg 45 | # 46 | # For details regarding assets in packages, see 47 | # https://flutter.dev/assets-and-images/#from-packages 48 | # 49 | # An image asset can refer to one or more resolution-specific "variants", see 50 | # https://flutter.dev/assets-and-images/#resolution-aware 51 | 52 | # To add custom fonts to your plugin package, add a fonts section here, 53 | # in this "flutter" section. Each entry in this list should have a 54 | # "family" key with the font family name, and a "fonts" key with a 55 | # list giving the asset and other descriptors for the font. For 56 | # example: 57 | # fonts: 58 | # - family: Schyler 59 | # fonts: 60 | # - asset: fonts/Schyler-Regular.ttf 61 | # - asset: fonts/Schyler-Italic.ttf 62 | # style: italic 63 | # - family: Trajan Pro 64 | # fonts: 65 | # - asset: fonts/TrajanPro.ttf 66 | # - asset: fonts/TrajanPro_Bold.ttf 67 | # weight: 700 68 | # 69 | # For details regarding fonts in packages, see 70 | # https://flutter.dev/custom-fonts/#from-packages 71 | -------------------------------------------------------------------------------- /plugins/proxy/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ 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 | -------------------------------------------------------------------------------- /plugins/proxy/windows/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.ftl": "vue-html", 4 | "*.vue": "vue", 5 | "*.scss": "sass", 6 | "*.md": "markdown", 7 | ".gitignore": "ignore", 8 | "atomic": "cpp", 9 | "bit": "cpp", 10 | "cctype": "cpp", 11 | "clocale": "cpp", 12 | "cmath": "cpp", 13 | "compare": "cpp", 14 | "concepts": "cpp", 15 | "cstddef": "cpp", 16 | "cstdint": "cpp", 17 | "cstdio": "cpp", 18 | "cstdlib": "cpp", 19 | "cstring": "cpp", 20 | "ctime": "cpp", 21 | "cwchar": "cpp", 22 | "exception": "cpp", 23 | "initializer_list": "cpp", 24 | "ios": "cpp", 25 | "iosfwd": "cpp", 26 | "istream": "cpp", 27 | "iterator": "cpp", 28 | "limits": "cpp", 29 | "memory": "cpp", 30 | "new": "cpp", 31 | "ostream": "cpp", 32 | "sstream": "cpp", 33 | "stdexcept": "cpp", 34 | "streambuf": "cpp", 35 | "string": "cpp", 36 | "system_error": "cpp", 37 | "tuple": "cpp", 38 | "type_traits": "cpp", 39 | "typeinfo": "cpp", 40 | "utility": "cpp", 41 | "variant": "cpp", 42 | "xfacet": "cpp", 43 | "xiosbase": "cpp", 44 | "xlocale": "cpp", 45 | "xlocinfo": "cpp", 46 | "xlocnum": "cpp", 47 | "xmemory": "cpp", 48 | "xstddef": "cpp", 49 | "xstring": "cpp", 50 | "xtr1common": "cpp", 51 | "xutility": "cpp" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /plugins/proxy/windows/include/proxy/proxy_plugin_c_api.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_PLUGIN_PROXY_PLUGIN_C_API_H_ 2 | #define FLUTTER_PLUGIN_PROXY_PLUGIN_C_API_H_ 3 | 4 | #include 5 | 6 | #ifdef FLUTTER_PLUGIN_IMPL 7 | #define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) 8 | #else 9 | #define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) 10 | #endif 11 | 12 | #if defined(__cplusplus) 13 | extern "C" { 14 | #endif 15 | 16 | FLUTTER_PLUGIN_EXPORT void ProxyPluginCApiRegisterWithRegistrar( 17 | FlutterDesktopPluginRegistrarRef registrar); 18 | 19 | #if defined(__cplusplus) 20 | } // extern "C" 21 | #endif 22 | 23 | #endif // FLUTTER_PLUGIN_PROXY_PLUGIN_C_API_H_ 24 | -------------------------------------------------------------------------------- /plugins/proxy/windows/proxy_plugin.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_PLUGIN_PROXY_PLUGIN_H_ 2 | #define FLUTTER_PLUGIN_PROXY_PLUGIN_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace proxy { 10 | 11 | class ProxyPlugin : public flutter::Plugin { 12 | public: 13 | static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); 14 | 15 | ProxyPlugin(); 16 | 17 | virtual ~ProxyPlugin(); 18 | 19 | // Disallow copy and assign. 20 | ProxyPlugin(const ProxyPlugin&) = delete; 21 | ProxyPlugin& operator=(const ProxyPlugin&) = delete; 22 | 23 | // Called when a method is called on this plugin's channel from Dart. 24 | void HandleMethodCall( 25 | const flutter::MethodCall &method_call, 26 | std::unique_ptr> result); 27 | }; 28 | 29 | } // namespace proxy 30 | 31 | #endif // FLUTTER_PLUGIN_PROXY_PLUGIN_H_ 32 | -------------------------------------------------------------------------------- /plugins/proxy/windows/proxy_plugin_c_api.cpp: -------------------------------------------------------------------------------- 1 | #include "include/proxy/proxy_plugin_c_api.h" 2 | 3 | #include 4 | 5 | #include "proxy_plugin.h" 6 | 7 | void ProxyPluginCApiRegisterWithRegistrar( 8 | FlutterDesktopPluginRegistrarRef registrar) { 9 | proxy::ProxyPlugin::RegisterWithRegistrar( 10 | flutter::PluginRegistrarManager::GetInstance() 11 | ->GetRegistrar(registrar)); 12 | } 13 | -------------------------------------------------------------------------------- /plugins/proxy/windows/test/proxy_plugin_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "proxy_plugin.h" 12 | 13 | namespace proxy { 14 | namespace test { 15 | 16 | namespace { 17 | 18 | using flutter::EncodableMap; 19 | using flutter::EncodableValue; 20 | using flutter::MethodCall; 21 | using flutter::MethodResultFunctions; 22 | 23 | } // namespace 24 | 25 | TEST(ProxyPlugin, GetPlatformVersion) { 26 | ProxyPlugin plugin; 27 | // Save the reply value from the success callback. 28 | std::string result_string; 29 | plugin.HandleMethodCall( 30 | MethodCall("getPlatformVersion", std::make_unique()), 31 | std::make_unique>( 32 | [&result_string](const EncodableValue* result) { 33 | result_string = std::get(*result); 34 | }, 35 | nullptr, nullptr)); 36 | 37 | // Since the exact string varies by host, just ensure that it's a string 38 | // with the expected format. 39 | EXPECT_TRUE(result_string.rfind("Windows ", 0) == 0); 40 | } 41 | 42 | } // namespace test 43 | } // namespace proxy 44 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: fl_clash 2 | description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. 3 | publish_to: 'none' 4 | version: 0.8.63+202410081 5 | environment: 6 | sdk: '>=3.1.0 <4.0.0' 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | flutter_localizations: 12 | sdk: flutter 13 | intl: ^0.19.0 14 | path_provider: ^2.1.0 15 | path: ^1.8.3 16 | shared_preferences: ^2.2.0 17 | provider: ^6.0.5 18 | window_manager: ^0.3.8 19 | dynamic_color: ^1.7.0 20 | proxy: 21 | path: plugins/proxy 22 | launch_at_startup: ^0.2.2 23 | windows_single_instance: ^1.0.1 24 | json_annotation: ^4.9.0 25 | file_picker: ^8.0.3 26 | mobile_scanner: ^5.1.1 27 | app_links: ^3.5.0 28 | win32_registry: ^1.1.2 29 | tray_manager: ^0.2.1 30 | collection: ^1.18.0 31 | animations: ^2.0.11 32 | package_info_plus: ^7.0.0 33 | url_launcher: ^6.2.6 34 | freezed_annotation: ^2.4.1 35 | image_picker: ^1.1.2 36 | zxing2: ^0.2.3 37 | image: ^4.1.7 38 | webdav_client: ^1.2.2 39 | dio: ^5.4.3+1 40 | win32: ^5.5.1 41 | ffi: ^2.1.2 42 | re_editor: ^0.3.1 43 | re_highlight: ^0.0.3 44 | archive: ^3.6.1 45 | lpinyin: ^2.0.3 46 | emoji_regex: ^0.0.5 47 | process_run: ^1.1.0 48 | cached_network_image: ^3.4.0 49 | hotkey_manager: ^0.2.3 50 | uni_platform: ^0.1.3 51 | device_info_plus: ^10.1.2 52 | dev_dependencies: 53 | flutter_test: 54 | sdk: flutter 55 | flutter_lints: ^3.0.1 56 | ffigen: ^11.0.0 57 | json_serializable: ^6.7.1 58 | build_runner: ^2.4.9 59 | args: ^2.4.2 60 | freezed: ^2.5.1 61 | 62 | flutter: 63 | uses-material-design: true 64 | assets: 65 | - assets/data/ 66 | - assets/fonts/ 67 | - assets/images/ 68 | - assets/images/avatars/ 69 | fonts: 70 | - family: Twemoji 71 | fonts: 72 | - asset: assets/fonts/Twemoji.Mozilla.ttf 73 | - family: Icons 74 | fonts: 75 | - asset: assets/fonts/Icons.ttf 76 | ffigen: 77 | name: "ClashFFI" 78 | output: 'lib/clash/generated/clash_ffi.dart' 79 | headers: 80 | entry-points: 81 | - 'libclash/android/arm64-v8a/libclash.h' 82 | flutter_intl: 83 | enabled: true 84 | class_name: AppLocalizations 85 | arb_dir: lib/l10n/arb 86 | output_dir: lib/l10n -------------------------------------------------------------------------------- /snapshots/desktop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/snapshots/desktop.gif -------------------------------------------------------------------------------- /snapshots/mobile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/snapshots/mobile.gif -------------------------------------------------------------------------------- /test/command_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'dart:io'; 4 | 5 | void main() { 6 | startService(); 7 | } 8 | 9 | startService() async { 10 | 11 | try { 12 | // 创建服务器 13 | final server = await HttpServer.bind("127.0.0.1", 10001); 14 | print('服务器正在监听 ${server.address.address}:${server.port}'); 15 | 16 | // 监听请求 17 | await for (HttpRequest request in server) { 18 | handleRequest(request); 19 | } 20 | } catch (e) { 21 | print('服务器错误: $e'); 22 | } 23 | } 24 | 25 | void handleRequest(HttpRequest request) { 26 | print(request.headers); 27 | // 处理请求 28 | request.response 29 | ..statusCode = HttpStatus.ok 30 | ..headers.contentType = ContentType.html 31 | ..write('

Hello, Dart Server!

'); 32 | 33 | // 完成响应 34 | request.response.close(); 35 | } 36 | -------------------------------------------------------------------------------- /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 | 19 | build/ 20 | 21 | out/ 22 | .idea/ 23 | .vs/ 24 | .vscode/ 25 | -------------------------------------------------------------------------------- /windows/EnableLoopback.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/windows/EnableLoopback.exe -------------------------------------------------------------------------------- /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 | #include 18 | #include 19 | 20 | void RegisterPlugins(flutter::PluginRegistry* registry) { 21 | AppLinksPluginCApiRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("AppLinksPluginCApi")); 23 | DynamicColorPluginCApiRegisterWithRegistrar( 24 | registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); 25 | FileSelectorWindowsRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("FileSelectorWindows")); 27 | HotkeyManagerWindowsPluginCApiRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); 29 | ProxyPluginCApiRegisterWithRegistrar( 30 | registry->GetRegistrarForPlugin("ProxyPluginCApi")); 31 | ScreenRetrieverPluginRegisterWithRegistrar( 32 | registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); 33 | TrayManagerPluginRegisterWithRegistrar( 34 | registry->GetRegistrarForPlugin("TrayManagerPlugin")); 35 | UrlLauncherWindowsRegisterWithRegistrar( 36 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 37 | WindowManagerPluginRegisterWithRegistrar( 38 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 39 | WindowsSingleInstancePluginRegisterWithRegistrar( 40 | registry->GetRegistrarForPlugin("WindowsSingleInstancePlugin")); 41 | } 42 | -------------------------------------------------------------------------------- /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 | app_links 7 | dynamic_color 8 | file_selector_windows 9 | hotkey_manager_windows 10 | proxy 11 | screen_retriever 12 | tray_manager 13 | url_launcher_windows 14 | window_manager 15 | windows_single_instance 16 | ) 17 | 18 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 19 | ) 20 | 21 | set(PLUGIN_BUNDLED_LIBRARIES) 22 | 23 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 25 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 26 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 27 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 28 | endforeach(plugin) 29 | 30 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 31 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 32 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 33 | endforeach(ffi_plugin) 34 | -------------------------------------------------------------------------------- /windows/packaging/exe/make_config.yaml: -------------------------------------------------------------------------------- 1 | app_id: 728B3532-C74B-4870-9068-BE70FE12A3E6 2 | app_name: FlClash 3 | publisher: chen08209 4 | publisher_url: https://github.com/chen08209/FlClash 5 | display_name: FlClash 6 | executable_name: FlClash.exe 7 | output_base_file_name: FlClash.exe 8 | setup_icon_file: ..\windows\runner\resources\app_icon.ico 9 | locales: 10 | - lang: zh 11 | file: ..\windows\packaging\exe\ChineseSimplified.isl 12 | - lang: en -------------------------------------------------------------------------------- /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 | 10 | 11 | add_executable(${BINARY_NAME} WIN32 12 | "flutter_window.cpp" 13 | "main.cpp" 14 | "utils.cpp" 15 | "win32_window.cpp" 16 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 17 | "Runner.rc" 18 | "runner.exe.manifest" 19 | ) 20 | 21 | # Apply the standard set of build settings. This can be removed for applications 22 | # that need different build settings. 23 | apply_standard_settings(${BINARY_NAME}) 24 | # Add preprocessor definitions for the build version. 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 29 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 30 | 31 | # Disable Windows macros that collide with C++ standard library functions. 32 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 33 | 34 | # Add dependency libraries and include directories. Add any application-specific 35 | # dependencies here. 36 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 37 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 38 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 39 | 40 | # Run the Flutter tool portions of the build. This must not be removed. 41 | add_dependencies(${BINARY_NAME} flutter_assemble) 42 | -------------------------------------------------------------------------------- /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 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /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 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"FlClash", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /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/Crazycrystaldev/FlClash/8d4931c09365b47b2d799f3cbf705b9c697609c7/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 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------