├── .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 | [](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
10 |
11 | [](https://t.me/FlClash)
12 |
13 |
14 | 基于ClashMeta的多平台代理客户端,简单易用,开源无广告。
15 |
16 | on Desktop:
17 |
18 |
19 |
20 |
21 | on Mobile:
22 |
23 |
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 |
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 |
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 |
--------------------------------------------------------------------------------