Compare commits

...

12 Commits

Author SHA1 Message Date
chen08209
6f8a9039b6 Add rule override
Optimize more details
2025-04-05 21:56:53 +08:00
chen08209
de9c5ba9cc Optimize dashboard performance
Fix some issues
2025-03-10 18:41:42 +08:00
chen08209
2aae00cf68 Fix unselected proxy group delay issues 2025-03-10 18:41:42 +08:00
chen08209
68be2d34a1 Fix asn url issues 2025-03-08 04:22:32 +08:00
chen08209
7895ccf720 Update changelog 2025-03-07 16:03:59 +00:00
chen08209
e92900dbbd Fix tab delay view issues
Fix tray action issues

Fix get profile redirect client ua issues

Fix proxy card delay view issues

Add Russian, Japanese adaptation

Fix some issues
2025-03-07 23:49:27 +08:00
chen08209
eada271c49 Update changelog 2025-03-05 07:22:18 +00:00
chen08209
5dda2854be Fix list form input view issues
Fix traffic view issues
2025-03-05 15:11:19 +08:00
chen08209
5184ed6fc7 Update changelog 2025-03-05 02:36:31 +00:00
chen08209
4e679f776e Optimize performance
Update core

Optimize core stability

Fix linux tun authority check error

Fix some issues
2025-03-05 10:21:51 +08:00
chen08209
96328f66e9 Fix scroll physics error 2025-02-09 16:51:57 +08:00
chen08209
3eb14ab8a1 Update changelog 2025-02-09 08:36:14 +00:00
190 changed files with 22809 additions and 10592 deletions

View File

@@ -4,6 +4,8 @@ on:
push:
tags:
- 'v*'
env:
IS_STABLE: ${{ !contains(github.ref, '-') }}
jobs:
build:
@@ -67,7 +69,6 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: 3.24.5
channel: stable
cache: true
@@ -75,7 +76,7 @@ jobs:
run: flutter pub get
- name: Setup
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }}
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }}
- name: Upload
uses: actions/upload-artifact@v4
@@ -89,14 +90,13 @@ jobs:
needs: [ build ]
steps:
- name: Checkout
if: ${{ !contains(github.ref, '+') }}
uses: actions/checkout@v4
if: ${{ env.IS_STABLE == 'true' }}
with:
fetch-depth: 0
ref: refs/heads/main
- name: Generate
if: ${{ !contains(github.ref, '+') }}
if: ${{ env.IS_STABLE == 'true' }}
run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
@@ -128,7 +128,7 @@ jobs:
cat NEW_CHANGELOG.md > CHANGELOG.md
- name: Commit
if: ${{ !contains(github.ref, '+') }}
if: ${{ env.IS_STABLE == 'true' }}
run: |
git add CHANGELOG.md
if ! git diff --cached --quiet; then
@@ -207,7 +207,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install requests
python release.py
python release_telegram.py
- name: Patch release.md
run: |
@@ -215,21 +215,21 @@ jobs:
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
- name: Release
if: ${{ !contains(github.ref, '+') }}
if: ${{ env.IS_STABLE == 'true' }}
uses: softprops/action-gh-release@v2
with:
files: ./dist/*
body_path: './release.md'
- name: Create Fdroid Source Dir
if: ${{ !contains(github.ref, '+') }}
if: ${{ env.IS_STABLE == 'true' }}
run: |
mkdir -p ./tmp
cp ./dist/*android-arm64-v8a* ./tmp/ || true
echo "Files copied successfully"
- name: Push to fdroid repo
if: ${{ !contains(github.ref, '+') }}
if: ${{ env.IS_STABLE == 'true' }}
uses: cpina/github-action-push-to-another-repository@v1.7.2
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
@@ -239,7 +239,7 @@ jobs:
destination-repository-name: FlClash-fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: action-pr
target-branch: main
commit-message: Update from ${{ github.ref_name }}
target-directory: /tmp/

6
.gitmodules vendored
View File

@@ -6,3 +6,9 @@
path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git
branch = FlClash
[submodule "plugins/tray_manager"]
path = plugins/tray_manager
url = git@github.com:chen08209/tray_manager.git
branch = main

View File

@@ -1,3 +1,55 @@
## v0.8.79
- Fix tab delay view issues
- Fix tray action issues
- Fix get profile redirect client ua issues
- Fix proxy card delay view issues
- Add Russian, Japanese adaptation
- Fix some issues
- Update changelog
## v0.8.78
- Fix list form input view issues
- Fix traffic view issues
- Update changelog
## v0.8.77
- Optimize performance
- Update core
- Optimize core stability
- Fix linux tun authority check error
- Fix some issues
- Fix scroll physics error
- Update changelog
## v0.8.75
- Add windows storage corruption detection
- Fix core crash caused by windows resource manager restart
- Optimize logs, requests, access to pages
- Fix macos bypass domain issues
- Update changelog
## v0.8.74
- Fix some issues

View File

@@ -1,29 +1 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -33,7 +33,7 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
android {
namespace "com.follow.clash"
compileSdkVersion 34
compileSdkVersion 35
ndkVersion "27.1.12297006"
compileOptions {
@@ -63,7 +63,7 @@ android {
defaultConfig {
applicationId "com.follow.clash"
minSdkVersion 21
targetSdkVersion 34
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

View File

@@ -10,14 +10,12 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
@@ -64,7 +62,9 @@
</intent-filter>
</activity>
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<activity
android:name=".TempActivity"
@@ -87,7 +87,6 @@
<service
android:name=".services.FlClashTileService"
android:exported="true"
android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -125,7 +124,7 @@
<service
android:name=".services.FlClashVpnService"
android:exported="false"
android:foregroundServiceType="specialUse"
android:foregroundServiceType="dataSync"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
@@ -138,7 +137,7 @@
<service
android:name=".services.FlClashService"
android:exported="false"
android:foregroundServiceType="specialUse">
android:foregroundServiceType="dataSync">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="service" />

View File

@@ -1,7 +1,7 @@
package com.follow.clash;
import android.app.Application
import android.content.Context;
import android.content.Context
class FlClashApplication : Application() {
companion object {

View File

@@ -1,9 +1,7 @@
package com.follow.clash
import android.content.Context
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin
import io.flutter.FlutterInjector

View File

@@ -7,6 +7,7 @@ enum class AccessControlMode {
}
data class AccessControl(
val enable: Boolean,
val mode: AccessControlMode,
val acceptList: List<String>,
val rejectList: List<String>,
@@ -17,7 +18,7 @@ data class CIDR(val address: InetAddress, val prefixLength: Int)
data class VpnOptions(
val enable: Boolean,
val port: Int,
val accessControl: AccessControl?,
val accessControl: AccessControl,
val allowBypass: Boolean,
val systemProxy: Boolean,
val bypassDomain: List<String>,

View File

@@ -291,16 +291,18 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun getPackages(): List<Package> {
val packageManager = FlClashApplication.getAppContext().packageManager
if (packages.isNotEmpty()) return packages
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != FlClashApplication.getAppContext().packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
?.filter {
it.packageName != FlClashApplication.getAppContext().packageName && (
it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
)
}?.map {
}?.map {
Package(
packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
label = it.applicationInfo?.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
lastUpdateTime = it.lastUpdateTime
)
}?.let { packages.addAll(it) }
@@ -353,7 +355,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
suspend fun getText(text: String): String? {
return withContext(Dispatchers.Default){
return withContext(Dispatchers.Default) {
channel.awaitResult<String>("getText", text)
}
}
@@ -391,31 +393,33 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}.forEach {
if (it.name.matches(chinaAppRegex)) return true
}
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
for (packageEntry in it.entries()) {
if (packageEntry.name.startsWith("firebase-")) return false
}
for (packageEntry in it.entries()) {
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
".dex"
))
) {
continue
packageInfo.applicationInfo?.publicSourceDir?.let {
ZipFile(File(it)).use {
for (packageEntry in it.entries()) {
if (packageEntry.name.startsWith("firebase-")) return false
}
if (packageEntry.size > 15000000) {
return true
}
val input = it.getInputStream(packageEntry).buffered()
val dexFile = try {
DexBackedDexFile.fromInputStream(null, input)
} catch (e: Exception) {
return false
}
for (clazz in dexFile.classes) {
val clazzName =
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
.replace("$", ".")
if (clazzName.matches(chinaAppRegex)) return true
for (packageEntry in it.entries()) {
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
".dex"
))
) {
continue
}
if (packageEntry.size > 15000000) {
return true
}
val input = it.getInputStream(packageEntry).buffered()
val dexFile = try {
DexBackedDexFile.fromInputStream(null, input)
} catch (e: Exception) {
return false
}
for (clazz in dexFile.classes) {
val clazzName =
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
.replace("$", ".")
if (clazzName.matches(chinaAppRegex)) return true
}
}
}
}

View File

@@ -1,6 +1,5 @@
package com.follow.clash.plugins
import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState
import com.follow.clash.models.VpnOptions
import com.google.gson.Gson

View File

@@ -92,11 +92,13 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null) {
if (flClashService is FlClashVpnService) {
if (fd != null && flClashService is FlClashVpnService) {
try {
(flClashService as FlClashVpnService).protect(fd)
result.success(true)
} catch (e: RuntimeException) {
result.success(false)
}
result.success(true)
} else {
result.success(false)
}

View File

@@ -7,7 +7,7 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Binder
import android.os.Build
import android.os.IBinder
@@ -87,6 +87,7 @@ class FlClashService : Service(), BaseServiceInterface {
}
}
}
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
return notificationBuilderDeferred.await()
}
@@ -100,7 +101,8 @@ class FlClashService : Service(), BaseServiceInterface {
}
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
@SuppressLint("ForegroundServiceType")
override suspend fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
@@ -116,7 +118,11 @@ class FlClashService : Service(), BaseServiceInterface {
.setContentTitle(title)
.setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
try {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} catch (_: Exception) {
startForeground(notificationId, notification)
}
} else {
startForeground(notificationId, notification)
}

View File

@@ -6,7 +6,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Binder
@@ -45,9 +45,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) {
routeAddress.forEach { i ->
Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}")
addRoute(i.address, i.prefixLength)
try {
routeAddress.forEach { i ->
Log.d(
"addRoute4",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("0.0.0.0", 0)
}
} else {
addRoute("0.0.0.0", 0)
@@ -58,9 +65,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
routeAddress.forEach { i ->
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}")
addRoute(i.address, i.prefixLength)
try {
routeAddress.forEach { i ->
Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("::", 0)
}
} else {
addRoute("::", 0)
@@ -68,17 +82,19 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
}
addDnsServer(options.dnsServerAddress)
setMtu(9000)
options.accessControl?.let { accessControl ->
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
options.accessControl.let { accessControl ->
if (accessControl.enable) {
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
}
}
AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it)
AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it)
}
}
}
}
@@ -163,7 +179,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
return notificationBuilderDeferred.await()
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
@SuppressLint("ForegroundServiceType")
override suspend fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
@@ -180,7 +196,11 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
.setContentText(content)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
try {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} catch (_: Exception) {
startForeground(notificationId, notification)
}
} else {
startForeground(notificationId, notification)
}

View File

@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
kotlin_version=1.9.22
agp_version=8.2.1
agp_version=8.9.1

View File

@@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

Binary file not shown.

Binary file not shown.

View File

@@ -5,6 +5,7 @@ targets:
options:
build_extensions:
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
freezed:
options:
build_extensions:

View File

@@ -22,99 +22,99 @@ func (result ActionResult) Json() ([]byte, error) {
return data, err
}
func (action Action) wrapMessage(data interface{}) []byte {
sendAction := ActionResult{
func (action Action) getResult(data interface{}) []byte {
resultAction := ActionResult{
Id: action.Id,
Method: action.Method,
Data: data,
}
res, _ := sendAction.Json()
res, _ := resultAction.Json()
return res
}
func handleAction(action *Action, send func([]byte)) {
func handleAction(action *Action, result func(data interface{})) {
switch action.Method {
case initClashMethod:
data := action.Data.(string)
send(action.wrapMessage(handleInitClash(data)))
result(handleInitClash(data))
return
case getIsInitMethod:
send(action.wrapMessage(handleGetIsInit()))
result(handleGetIsInit())
return
case forceGcMethod:
handleForceGc()
send(action.wrapMessage(true))
result(true)
return
case shutdownMethod:
send(action.wrapMessage(handleShutdown()))
result(handleShutdown())
return
case validateConfigMethod:
data := []byte(action.Data.(string))
send(action.wrapMessage(handleValidateConfig(data)))
result(handleValidateConfig(data))
return
case updateConfigMethod:
data := []byte(action.Data.(string))
send(action.wrapMessage(handleUpdateConfig(data)))
result(handleUpdateConfig(data))
return
case getProxiesMethod:
send(action.wrapMessage(handleGetProxies()))
result(handleGetProxies())
return
case changeProxyMethod:
data := action.Data.(string)
handleChangeProxy(data, func(value string) {
send(action.wrapMessage(value))
result(value)
})
return
case getTrafficMethod:
send(action.wrapMessage(handleGetTraffic()))
result(handleGetTraffic())
return
case getTotalTrafficMethod:
send(action.wrapMessage(handleGetTotalTraffic()))
result(handleGetTotalTraffic())
return
case resetTrafficMethod:
handleResetTraffic()
send(action.wrapMessage(true))
result(true)
return
case asyncTestDelayMethod:
data := action.Data.(string)
handleAsyncTestDelay(data, func(value string) {
send(action.wrapMessage(value))
result(value)
})
return
case getConnectionsMethod:
send(action.wrapMessage(handleGetConnections()))
result(handleGetConnections())
return
case closeConnectionsMethod:
send(action.wrapMessage(handleCloseConnections()))
result(handleCloseConnections())
return
case closeConnectionMethod:
id := action.Data.(string)
send(action.wrapMessage(handleCloseConnection(id)))
result(handleCloseConnection(id))
return
case getExternalProvidersMethod:
send(action.wrapMessage(handleGetExternalProviders()))
result(handleGetExternalProviders())
return
case getExternalProviderMethod:
externalProviderName := action.Data.(string)
send(action.wrapMessage(handleGetExternalProvider(externalProviderName)))
result(handleGetExternalProvider(externalProviderName))
case updateGeoDataMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
send(action.wrapMessage(err.Error()))
result(err.Error())
return
}
geoType := params["geo-type"]
geoName := params["geo-name"]
handleUpdateGeoData(geoType, geoName, func(value string) {
send(action.wrapMessage(value))
result(value)
})
return
case updateExternalProviderMethod:
providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) {
send(action.wrapMessage(value))
result(value)
})
return
case sideLoadExternalProviderMethod:
@@ -122,46 +122,56 @@ func handleAction(action *Action, send func([]byte)) {
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
send(action.wrapMessage(err.Error()))
result(err.Error())
return
}
providerName := params["providerName"]
data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
send(action.wrapMessage(value))
result(value)
})
return
case startLogMethod:
handleStartLog()
send(action.wrapMessage(true))
result(true)
return
case stopLogMethod:
handleStopLog()
send(action.wrapMessage(true))
result(true)
return
case startListenerMethod:
send(action.wrapMessage(handleStartListener()))
result(handleStartListener())
return
case stopListenerMethod:
send(action.wrapMessage(handleStopListener()))
result(handleStopListener())
return
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
send(action.wrapMessage(value))
result(value)
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
send(action.wrapMessage(value))
result(value)
})
return
case getProfileMethod:
profileId := action.Data.(string)
handleGetMemory(func(value string) {
result(handleGetProfile(profileId))
})
return
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
result(true)
default:
handle := nextHandle(action, send)
handle := nextHandle(action, result)
if handle {
return
} else {
send(action.wrapMessage(action.DefaultValue))
result(action.DefaultValue)
}
}
}

View File

@@ -28,10 +28,22 @@ import (
"sync"
)
func splitByMultipleSeparators(s string) interface{} {
isSeparator := func(r rune) bool {
return r == ',' || r == ' ' || r == ';'
}
parts := strings.FieldsFunc(s, isSeparator)
if len(parts) > 1 {
return parts
}
return s
}
var (
isRunning = false
runLock sync.Mutex
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"}
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
)
@@ -160,9 +172,19 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
return prof
}
func genHosts(hosts, patchHosts map[string]any) {
func attachHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts {
hosts[k] = v
if str, ok := v.(string); ok {
hosts[k] = splitByMultipleSeparators(str)
}
}
}
func updatePatchDns(dns config.RawDNS) {
for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() {
if str, ok := pair.Value.(string); ok {
dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str))
}
}
}
@@ -173,26 +195,25 @@ func trimArr(arr []string) (r []string) {
return
}
func overrideRules(rules *[]string) {
var target = ""
for _, line := range *rules {
func overrideRules(rules, patchRules []string) []string {
target := ""
for _, line := range rules {
rule := trimArr(strings.Split(line, ","))
l := len(rule)
if l != 2 {
return
if len(rule) != 2 {
continue
}
if strings.ToUpper(rule[0]) == "MATCH" {
if strings.EqualFold(rule[0], "MATCH") {
target = rule[1]
break
}
}
if target == "" {
return
return rules
}
var rulesExt = lo.Map(ips, func(ip string, index int) string {
return fmt.Sprintf("DOMAIN %s %s", ip, target)
rulesExt := lo.Map(ips, func(ip string, _ int) string {
return fmt.Sprintf("DOMAIN,%s,%s", ip, target)
})
*rules = append(rulesExt, *rules...)
return append(append(rulesExt, patchRules...), rules...)
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
@@ -215,6 +236,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl
@@ -225,15 +247,20 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
for idx := range targetConfig.ProxyGroup {
targetConfig.ProxyGroup[idx]["url"] = ""
}
genHosts(targetConfig.Hosts, patchConfig.Hosts)
attachHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns {
updatePatchDns(patchConfig.DNS)
targetConfig.DNS = patchConfig.DNS
} else {
if targetConfig.DNS.Enable == false {
targetConfig.DNS.Enable = true
}
}
overrideRules(&targetConfig.Rule)
if configParams.OverrideRule {
targetConfig.Rule = overrideRules(patchConfig.Rule, []string{})
} else {
targetConfig.Rule = overrideRules(targetConfig.Rule, patchConfig.Rule)
}
}
func patchConfig() {
@@ -247,6 +274,7 @@ func patchConfig() {
dialer.DefaultInterface.Store(general.Interface)
adapter.UnifiedDelay.Store(general.UnifiedDelay)
tunnel.SetMode(general.Mode)
tunnel.UpdateRules(currentConfig.Rules, currentConfig.SubRules, currentConfig.RuleProviders)
log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6

View File

@@ -8,12 +8,12 @@ import (
)
type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
OverrideRule bool `json:"override-rule"`
}
type GenerateConfigParams struct {
@@ -80,6 +80,7 @@ const (
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
getRunTimeMethod Method = "getRunTime"
getCurrentProfileNameMethod Method = "getCurrentProfileName"
getProfileMethod Method = "getProfile"
)
type Method string

View File

@@ -6,7 +6,7 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
require (
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
github.com/samber/lo v1.47.0
github.com/samber/lo v1.49.1
)
require (
@@ -19,28 +19,28 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/enfein/mieru/v3 v3.10.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/enfein/mieru/v3 v3.13.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.0 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect
github.com/gofrs/uuid/v5 v5.3.1 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
@@ -50,22 +50,24 @@ require (
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bart v0.19.0 // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.0 // indirect
github.com/metacubex/chacha v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.4.5 // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // indirect
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
github.com/metacubex/utls v1.6.6 // indirect
github.com/metacubex/utls v1.6.8-alpha.4 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
@@ -73,18 +75,18 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing v0.5.1 // indirect
github.com/sagernet/sing v0.5.2 // indirect
github.com/sagernet/sing-mux v0.2.1 // indirect
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/shirou/gopsutil/v4 v4.24.11 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
@@ -101,13 +103,13 @@ require (
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect

View File

@@ -24,12 +24,12 @@ github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ=
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -43,8 +43,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -74,8 +74,8 @@ github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@@ -84,6 +84,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -96,40 +97,45 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc=
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
github.com/metacubex/utls v1.6.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI=
github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
@@ -149,8 +155,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
@@ -164,18 +170,18 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJ
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
@@ -218,8 +224,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -228,11 +234,11 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -248,12 +254,12 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -263,8 +269,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,6 +2,7 @@ package main
import (
"context"
"core/state"
"encoding/json"
"fmt"
"github.com/metacubex/mihomo/adapter"
@@ -26,10 +27,8 @@ import (
)
var (
isInit = false
configParams = ConfigExtendedParams{
OnlyStatisticsProxy: false,
}
isInit = false
configParams = ConfigExtendedParams{}
externalProviders = map[string]cp.Provider{}
logSubscriber observable.Subscription[log.Event]
currentConfig *config.Config
@@ -152,7 +151,7 @@ func handleChangeProxy(data string, fn func(string string)) {
}
func handleGetTraffic() string {
up, down := statistic.DefaultManager.Current(configParams.OnlyStatisticsProxy)
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -166,7 +165,7 @@ func handleGetTraffic() string {
}
func handleGetTotalTraffic() string {
up, down := statistic.DefaultManager.Total(configParams.OnlyStatisticsProxy)
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -179,6 +178,15 @@ func handleGetTotalTraffic() string {
return string(data)
}
func handleGetProfile(profileId string) string {
prof := getRawConfigWithId(profileId)
data, err := json.Marshal(prof)
if err != nil {
return ""
}
return string(data)
}
func handleResetTraffic() {
statistic.DefaultManager.ResetStatistic()
}
@@ -220,6 +228,7 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
if params.TestUrl != "" {
testUrl = params.TestUrl
}
delayData.Url = testUrl
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
if err != nil || delay == 0 {
@@ -423,6 +432,10 @@ func handleGetMemory(fn func(value string)) {
}()
}
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func init() {
adapter.UrlTestHook = func(url string, name string, delay uint16) {
delayData := &Delay{

View File

@@ -49,8 +49,8 @@ func invokeAction(paramsChar *C.char, port C.longlong) {
bridge.SendToPort(i, err.Error())
return
}
go handleAction(action, func(bytes []byte) {
bridge.SendToPort(i, string(bytes))
go handleAction(action, func(data interface{}) {
bridge.SendToPort(i, string(action.getResult(data)))
})
}
@@ -64,7 +64,7 @@ func sendMessage(message Message) {
}
bridge.SendToPort(messagePort, string(Action{
Method: messageMethod,
}.wrapMessage(res)))
}.getResult(res)))
}
//export startListener

View File

@@ -52,18 +52,6 @@ func NewInvokeManager() *InvokeManager {
}
}
func (m *InvokeManager) load(id string) string {
res, ok := m.invokeMap.Load(id)
if ok {
return res.(string)
}
return ""
}
func (m *InvokeManager) delete(id string) {
m.invokeMap.Delete(id)
}
func (m *InvokeManager) completer(id string, value string) {
m.invokeMap.Store(id, value)
m.chanLock.Lock()
@@ -74,7 +62,7 @@ func (m *InvokeManager) completer(id string, value string) {
m.chanLock.Unlock()
}
func (m *InvokeManager) await(id string) {
func (m *InvokeManager) await(id string) string {
m.chanLock.Lock()
if _, ok := m.chanMap[id]; !ok {
m.chanMap[id] = make(chan struct{})
@@ -85,12 +73,17 @@ func (m *InvokeManager) await(id string) {
timeout := time.After(500 * time.Millisecond)
select {
case <-ch:
return
res, ok := m.invokeMap.Load(id)
m.invokeMap.Delete(id)
if ok {
return res.(string)
} else {
return ""
}
case <-timeout:
m.completer(id, "")
return
return ""
}
}
var (
@@ -195,7 +188,6 @@ func initSocketHook() {
})
fdInvokeMap.await(id)
fdInvokeMap.delete(id)
})
}
}
@@ -214,10 +206,7 @@ func init() {
Id: id,
Metadata: metadata,
})
processInvokeMap.await(id)
res := processInvokeMap.load(id)
processInvokeMap.delete(id)
return res, nil
return processInvokeMap.await(id), nil
}
}
@@ -225,14 +214,14 @@ func handleGetAndroidVpnOptions() string {
tunLock.Lock()
defer tunLock.Unlock()
options := state.AndroidVpnOptions{
Enable: state.CurrentState.Enable,
Enable: state.CurrentState.VpnProps.Enable,
Port: currentConfig.General.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.AccessControl,
SystemProxy: state.CurrentState.SystemProxy,
AllowBypass: state.CurrentState.AllowBypass,
RouteAddress: state.CurrentState.RouteAddress,
AccessControl: state.CurrentState.VpnProps.AccessControl,
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
RouteAddress: currentConfig.General.Tun.RouteAddress,
BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(),
}
@@ -244,10 +233,6 @@ func handleGetAndroidVpnOptions() string {
return string(data)
}
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func handleUpdateDns(value string) {
go func() {
log.Infoln("[DNS] updateDns %s", value)
@@ -263,46 +248,41 @@ func handleGetCurrentProfileName() string {
return state.CurrentState.CurrentProfileName
}
func nextHandle(action *Action, send func([]byte)) bool {
func nextHandle(action *Action, result func(data interface{})) bool {
switch action.Method {
case startTunMethod:
data := action.Data.(string)
var fd int
_ = json.Unmarshal([]byte(data), &fd)
send(action.wrapMessage(handleStartTun(fd)))
result(handleStartTun(fd))
return true
case stopTunMethod:
handleStopTun()
send(action.wrapMessage(true))
return true
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
send(action.wrapMessage(true))
result(true)
return true
case getAndroidVpnOptionsMethod:
send(action.wrapMessage(handleGetAndroidVpnOptions()))
result(handleGetAndroidVpnOptions())
return true
case updateDnsMethod:
data := action.Data.(string)
handleUpdateDns(data)
send(action.wrapMessage(true))
result(true)
return true
case setFdMapMethod:
fdId := action.Data.(string)
handleSetFdMap(fdId)
send(action.wrapMessage(true))
result(true)
return true
case setProcessMapMethod:
data := action.Data.(string)
handleSetProcessMap(data)
send(action.wrapMessage(true))
result(true)
return true
case getRunTimeMethod:
send(action.wrapMessage(handleGetRunTime()))
result(handleGetRunTime())
return true
case getCurrentProfileNameMethod:
send(action.wrapMessage(handleGetCurrentProfileName()))
result(handleGetCurrentProfileName())
return true
}
return false

View File

@@ -2,10 +2,6 @@
package main
func nextHandle(action *Action) {
return action
}
func nextHandle(action *Action, send func([]byte)) bool {
func nextHandle(action *Action, result func(data interface{})) bool {
return false
}

View File

@@ -19,7 +19,7 @@ func sendMessage(message Message) {
}
send(Action{
Method: messageMethod,
}.wrapMessage(res))
}.getResult(res))
}
func send(data []byte) {
@@ -61,12 +61,12 @@ func startServer(arg string) {
return
}
go handleAction(action, func(bytes []byte) {
send(bytes)
go handleAction(action, func(data interface{}) {
send(action.getResult(data))
})
}
}
func nextHandle(action *Action, send func([]byte)) bool {
func nextHandle(action *Action, result func(data interface{})) bool {
return false
}

View File

@@ -1,7 +1,7 @@
//go:build android && cgo
package state
import "net/netip"
var DefaultIpv4Address = "172.19.0.1/30"
var DefaultDnsAddress = "172.19.0.2"
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
@@ -13,13 +13,14 @@ type AndroidVpnOptions struct {
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
BypassDomain []string `json:"bypassDomain"`
RouteAddress []string `json:"routeAddress"`
RouteAddress []netip.Prefix `json:"routeAddress"`
Ipv4Address string `json:"ipv4Address"`
Ipv6Address string `json:"ipv6Address"`
DnsServerAddress string `json:"dnsServerAddress"`
}
type AccessControl struct {
Enable bool `json:"enable"`
Mode string `json:"mode"`
AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"`
@@ -31,20 +32,23 @@ type AndroidVpnRawOptions struct {
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
RouteAddress []string `json:"routeAddress"`
Ipv6 bool `json:"ipv6"`
BypassDomain []string `json:"bypassDomain"`
}
type State struct {
AndroidVpnRawOptions
CurrentProfileName string `json:"currentProfileName"`
VpnProps AndroidVpnRawOptions `json:"vpn-props"`
CurrentProfileName string `json:"current-profile-name"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
BypassDomain []string `json:"bypass-domain"`
}
var CurrentState = &State{}
var CurrentState = &State{
OnlyStatisticsProxy: false,
CurrentProfileName: "",
}
func GetIpv6Address() string {
if CurrentState.Ipv6 {
if CurrentState.VpnProps.Ipv6 {
return DefaultIpv6Address
} else {
return ""

View File

@@ -33,7 +33,7 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
}
prefix4 = append(prefix4, tempPrefix4)
var prefix6 []netip.Prefix
if state.CurrentState.Ipv6 {
if state.CurrentState.VpnProps.Ipv6 {
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
if err != nil {
log.Errorln("startTUN error:", err)

View File

@@ -7,57 +7,27 @@ import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'controller.dart';
import 'models/models.dart';
import 'pages/pages.dart';
runAppWithPreferences(
Widget child, {
required AppState appState,
required Config config,
required AppFlowingState appFlowingState,
required ClashConfig clashConfig,
}) {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<ClashConfig>(
create: (_) => clashConfig,
),
ChangeNotifierProvider<Config>(
create: (_) => config,
),
ChangeNotifierProvider<AppFlowingState>(
create: (_) => appFlowingState,
),
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
create: (_) => appState,
update: (_, config, clashConfig, appState) {
appState?.mode = clashConfig.mode;
appState?.selectedMap = config.currentSelectedMap;
return appState!;
},
)
],
child: child,
));
}
class Application extends StatefulWidget {
class Application extends ConsumerStatefulWidget {
const Application({
super.key,
});
@override
State<Application> createState() => ApplicationState();
ConsumerState<Application> createState() => ApplicationState();
}
class ApplicationState extends State<Application> {
late SystemColorSchemes systemColorSchemes;
class ApplicationState extends ConsumerState<Application> {
late ColorSchemes systemColorSchemes;
Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer;
@@ -73,7 +43,7 @@ class ApplicationState extends State<Application> {
ColorScheme _getAppColorScheme({
required Brightness brightness,
int? primaryColor,
required SystemColorSchemes systemColorSchemes,
required ColorSchemes systemColorSchemes,
}) {
if (primaryColor != null) {
return ColorScheme.fromSeed(
@@ -81,7 +51,7 @@ class ApplicationState extends State<Application> {
brightness: brightness,
);
} else {
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness);
return systemColorSchemes.getColorSchemeForBrightness(brightness);
}
}
@@ -90,12 +60,11 @@ class ApplicationState extends State<Application> {
super.initState();
_autoUpdateGroupTask();
_autoUpdateProfilesTask();
globalState.appController = AppController(context);
globalState.measure = Measure.of(context);
globalState.appController = AppController(context, ref);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) {
globalState.appController = AppController(currentContext);
globalState.appController = AppController(currentContext, ref);
}
await globalState.appController.init();
globalState.appController.initLink();
@@ -119,7 +88,7 @@ class ApplicationState extends State<Application> {
});
}
_buildPlatformWrap(Widget child) {
_buildPlatformState(Widget child) {
if (system.isDesktop) {
return WindowManager(
child: TrayManager(
@@ -138,18 +107,7 @@ class ApplicationState extends State<Application> {
);
}
_buildPage(Widget page) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: page,
);
}
return VpnManager(
child: page,
);
}
_buildWrap(Widget child) {
_buildState(Widget child) {
return AppStateManager(
child: ClashManager(
child: ConnectivityManager(
@@ -163,11 +121,30 @@ class ApplicationState extends State<Application> {
);
}
_buildPlatformApp(Widget child) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: child,
);
}
return VpnManager(
child: child,
);
}
_buildApp(Widget child) {
return MessageManager(
child: ThemeManager(
child: child,
),
);
}
_updateSystemColorSchemes(
ColorScheme? lightDynamic,
ColorScheme? darkDynamic,
) {
systemColorSchemes = SystemColorSchemes(
systemColorSchemes = ColorSchemes(
lightColorScheme: lightDynamic,
darkColorScheme: darkDynamic,
);
@@ -178,21 +155,18 @@ class ApplicationState extends State<Application> {
@override
Widget build(context) {
return _buildPlatformWrap(
_buildWrap(
Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.appSetting.locale,
themeMode: config.themeProps.themeMode,
primaryColor: config.themeProps.primaryColor,
prueBlack: config.themeProps.prueBlack,
fontFamily: config.themeProps.fontFamily,
),
builder: (_, state, child) {
return _buildPlatformState(
_buildState(
Consumer(
builder: (_, ref, child) {
final locale =
ref.watch(appSettingProvider.select((state) => state.locale));
final themeProps = ref.watch(themeSettingProvider);
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
@@ -201,43 +175,34 @@ class ApplicationState extends State<Application> {
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
return MessageManager(
child: LayoutBuilder(
builder: (_, container) {
final appController = globalState.appController;
final maxWidth = container.maxWidth;
if (appController.appState.viewWidth != maxWidth) {
globalState.appController.updateViewWidth(maxWidth);
}
return _buildPage(child!);
},
return AppEnvManager(
child: _buildPlatformApp(
_buildApp(child!),
),
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(state.locale),
locale: other.getLocaleForString(locale),
supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
themeMode: themeProps.themeMode,
theme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
primaryColor: themeProps.primaryColor,
),
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
).toPrueBlack(state.prueBlack),
primaryColor: themeProps.primaryColor,
).toPureBlack(themeProps.pureBlack),
),
home: child,
);

View File

@@ -63,15 +63,16 @@ class ClashCore {
}
}
Future<bool> init({
required ClashConfig clashConfig,
required Config config,
}) async {
Future<bool> init() async {
await initGeo();
final homeDirPath = await appPath.homeDirPath;
return await clashInterface.init(homeDirPath);
}
Future<bool> setState(CoreState state) async {
return await clashInterface.setState(state);
}
shutdown() async {
await clashInterface.shutdown();
}
@@ -234,6 +235,14 @@ class ClashCore {
return int.parse(value);
}
Future<ClashConfigSnippet?> getProfile(String id) async {
final res = await clashInterface.getProfile(id);
if (res.isEmpty) {
return null;
}
return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res)));
}
resetTraffic() {
clashInterface.resetTraffic();
}

View File

@@ -2,12 +2,9 @@ import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/clash/message.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/common/future.dart';
import 'package:fl_clash/common/other.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart' hide Action;
mixin ClashInterface {
Future<bool> init(String homeDir);
@@ -66,6 +63,10 @@ mixin ClashInterface {
FutureOr<bool> closeConnection(String id);
FutureOr<bool> closeConnections();
FutureOr<String> getProfile(String id);
Future<bool> setState(CoreState state);
}
mixin AndroidClashInterface {
@@ -73,8 +74,6 @@ mixin AndroidClashInterface {
Future<bool> setProcessMap(ProcessMapItem item);
Future<bool> setState(CoreState state);
Future<bool> stopTun();
Future<bool> updateDns(String value);
@@ -106,6 +105,7 @@ abstract class ClashHandlerInterface with ClashInterface {
case ActionMethod.closeConnections:
case ActionMethod.closeConnection:
case ActionMethod.stopListener:
case ActionMethod.setState:
completer?.complete(result.data as bool);
return;
case ActionMethod.changeProxy:
@@ -137,7 +137,7 @@ abstract class ClashHandlerInterface with ClashInterface {
completer?.complete(result.data);
}
} catch (_) {
debugPrint(result.id);
commonPrint.log(result.id);
}
}
@@ -198,6 +198,14 @@ abstract class ClashHandlerInterface with ClashInterface {
);
}
@override
Future<bool> setState(CoreState state) {
return invoke<bool>(
method: ActionMethod.setState,
data: json.encode(state),
);
}
@override
shutdown() async {
return await invoke<bool>(
@@ -232,6 +240,7 @@ abstract class ClashHandlerInterface with ClashInterface {
return await invoke<String>(
method: ActionMethod.updateConfig,
data: json.encode(updateConfigParams),
timeout: Duration(minutes: 2),
);
}
@@ -239,6 +248,7 @@ abstract class ClashHandlerInterface with ClashInterface {
Future<String> getProxies() {
return invoke<String>(
method: ActionMethod.getProxies,
timeout: Duration(seconds: 5),
);
}
@@ -318,6 +328,14 @@ abstract class ClashHandlerInterface with ClashInterface {
);
}
@override
Future<String> getProfile(String id) {
return invoke<String>(
method: ActionMethod.getProfile,
data: id,
);
}
@override
FutureOr<String> getTotalTraffic() {
return invoke<String>(

View File

@@ -67,7 +67,6 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
switch (result.method) {
case ActionMethod.setFdMap:
case ActionMethod.setProcessMap:
case ActionMethod.setState:
case ActionMethod.stopTun:
case ActionMethod.updateDns:
completer?.complete(result.data as bool);
@@ -123,14 +122,6 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
);
}
@override
Future<bool> setState(CoreState state) {
return invoke<bool>(
method: ActionMethod.setState,
data: json.encode(state),
);
}
@override
Future<DateTime?> startTun(int fd) async {
final res = await invoke<String>(
@@ -259,7 +250,7 @@ class ClashLibHandler {
setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
@@ -321,10 +312,10 @@ class ClashLibHandler {
}
Future<String> quickStart(
String homeDir,
UpdateConfigParams updateConfigParams,
CoreState state,
) {
String homeDir,
UpdateConfigParams updateConfigParams,
CoreState state,
) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {

View File

@@ -6,6 +6,7 @@ import 'dart:typed_data';
import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/state.dart';
class ClashService extends ClashHandlerInterface {
static ClashService? _instance;
@@ -14,6 +15,8 @@ class ClashService extends ClashHandlerInterface {
Completer<Socket> socketCompleter = Completer();
bool isStarting = false;
Process? process;
factory ClashService() {
@@ -27,48 +30,61 @@ class ClashService extends ClashHandlerInterface {
}
_initServer() async {
final address = !Platform.isWindows
? InternetAddress(
unixSocketPath,
type: InternetAddressType.unix,
)
: InternetAddress(
localhost,
type: InternetAddressType.IPv4,
);
await _deleteSocketFile();
final server = await ServerSocket.bind(
address,
0,
shared: true,
);
serverCompleter.complete(server);
await for (final socket in server) {
await _destroySocket();
socketCompleter.complete(socket);
socket
.transform(
StreamTransformer<Uint8List, String>.fromHandlers(
handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
runZonedGuarded(() async {
final address = !Platform.isWindows
? InternetAddress(
unixSocketPath,
type: InternetAddressType.unix,
)
: InternetAddress(
localhost,
type: InternetAddressType.IPv4,
);
await _deleteSocketFile();
final server = await ServerSocket.bind(
address,
0,
shared: true,
);
serverCompleter.complete(server);
await for (final socket in server) {
await _destroySocket();
socketCompleter.complete(socket);
socket
.transform(
StreamTransformer<Uint8List, String>.fromHandlers(
handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
),
)
.transform(LineSplitter())
.listen(
(data) {
handleResult(
ActionResult.fromJson(
json.decode(data.trim()),
),
);
},
),
)
.transform(LineSplitter())
.listen(
(data) {
handleResult(
ActionResult.fromJson(
json.decode(data.trim()),
),
);
},
);
}
);
}
}, (error, stack) {
commonPrint.log(error.toString());
if(error is SocketException){
globalState.showNotifier(error.toString());
globalState.appController.restartCore();
}
});
}
@override
reStart() async {
if (isStarting == true) {
return;
}
isStarting = true;
socketCompleter = Completer();
if (process != null) {
await shutdown();
}
@@ -90,6 +106,7 @@ class ClashService extends ClashHandlerInterface {
],
);
process!.stdout.listen((_) {});
isStarting = false;
}
@override
@@ -125,7 +142,6 @@ class ClashService extends ClashHandlerInterface {
@override
shutdown() async {
await super.shutdown();
if (Platform.isWindows) {
await request.stopCoreByHelper();
}

View File

@@ -1,21 +1,40 @@
import 'package:flutter/material.dart';
extension ColorExtension on Color {
Color get toLight {
return withOpacity(0.8);
Color get opacity80 {
return withAlpha(204);
}
Color get toLighter {
return withOpacity(0.6);
Color get opacity60 {
return withAlpha(153);
}
Color get toSoft {
return withOpacity(0.12);
Color get opacity50 {
return withAlpha(128);
}
Color get toLittle {
return withOpacity(0.03);
Color get opacity38 {
return withAlpha(97);
}
Color get opacity30 {
return withAlpha(77);
}
Color get opacity15 {
return withAlpha(38);
}
Color get opacity10 {
return withAlpha(15);
}
Color get opacity3 {
return withAlpha(76);
}
Color get opacity0 {
return withAlpha(0);
}
Color darken([double amount = .1]) {
@@ -51,7 +70,7 @@ extension ColorExtension on Color {
}
extension ColorSchemeExtension on ColorScheme {
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
ColorScheme toPureBlack(bool isPrueBlack) => isPrueBlack
? copyWith(
surface: Colors.black,
surfaceContainer: surfaceContainer.darken(

View File

@@ -35,4 +35,5 @@ export 'tray.dart';
export 'window.dart';
export 'windows.dart';
export 'render.dart';
export 'view.dart';
export 'mixin.dart';
export 'print.dart';

View File

@@ -11,6 +11,8 @@ import 'package:flutter/material.dart';
const appName = "FlClash";
const appHelperService = "FlClashHelperService";
const coreName = "clash.meta";
const browserUa =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890;
@@ -19,6 +21,11 @@ const baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
);
double textScaleFactor = min(
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
1.2,
);
const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
@@ -33,16 +40,6 @@ final double kHeaderHeight = system.isDesktop
? 40
: 28
: 0;
const GeoXMap defaultGeoXMap = {
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
"geosite":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
};
const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1";
const clashConfigKey = "clash_config";
@@ -53,10 +50,8 @@ const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090";
const maxMobileWidth = 600;
const maxLaptopWidth = 840;
const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard";
const defaultTestUrl = "https://www.gstatic.com/generate_204";
final filter = ImageFilter.blur(
final commonFilter = ImageFilter.blur(
sigmaX: 5,
sigmaY: 5,
tileMode: TileMode.mirror,
@@ -86,7 +81,7 @@ const viewModeColumnsMap = {
const defaultPrimaryColor = Colors.brown;
double getWidgetHeight(num lines) {
return max(lines * 84 + (lines - 1) * 16, 0);
return max(lines * 84 * textScaleFactor + (lines - 1) * 16, 0);
}
final mainIsolate = "FlClashMainIsolate";

View File

@@ -1,7 +1,7 @@
import 'dart:async';
class Debouncer {
final Map<dynamic, Timer> _operations = {};
final Map<dynamic, Timer?> _operations = {};
call(
dynamic tag,
@@ -28,14 +28,15 @@ class Debouncer {
cancel(dynamic tag) {
_operations[tag]?.cancel();
_operations[tag] = null;
}
}
class Throttler {
final Map<dynamic, Timer> _operations = {};
final Map<dynamic, Timer?> _operations = {};
call(
String tag,
dynamic tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
@@ -60,10 +61,27 @@ class Throttler {
cancel(dynamic tag) {
_operations[tag]?.cancel();
_operations[tag] = null;
}
}
Future<T> retry<T>({
required Future<T> Function() task,
int maxAttempts = 3,
required bool Function(T res) retryIf,
Duration delay = Duration.zero,
}) async {
int attempts = 0;
while (attempts < maxAttempts) {
final res = await task();
if (!retryIf(res) || attempts >= maxAttempts) {
return res;
}
attempts++;
}
throw "unknown error";
}
final debouncer = Debouncer();
final throttler = Throttler();
final throttler = Throttler();

View File

@@ -10,7 +10,7 @@ extension CompleterExt<T> on Completer<T> {
FutureOr<T> Function()? onTimeout,
required String functionName,
}) {
final realTimeout = timeout ?? const Duration(seconds: 1);
final realTimeout = timeout ?? const Duration(seconds: 30);
Timer(realTimeout + commonDuration, () {
if (onLast != null) {
onLast();

View File

@@ -1,26 +1,24 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import '../state.dart';
import 'constant.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
class FlClashHttpOverrides extends HttpOverrides {
static String handleFindProxy(Uri url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
final port = globalState.config.patchClashConfig.mixedPort;
final isStart = globalState.appState.runTime != null;
commonPrint.log("find $url proxy:$isStart");
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
}
@override
HttpClient createHttpClient(SecurityContext? context) {
final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true;
client.findProxy = (url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
final appController = globalState.appController;
final port = appController.clashConfig.mixedPort;
final isStart = appController.appFlowingState.isStart;
debugPrint("find $url proxy:$isStart");
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
};
client.findProxy = handleFindProxy;
return client;
}
}

View File

@@ -65,3 +65,12 @@ extension DoubleListExt on List<double> {
return -1;
}
}
extension MapExt<K, V> on Map<K, V> {
getCacheValue(K key, V defaultValue) {
if (this[key] == null) {
this[key] = defaultValue;
}
return this[key];
}
}

View File

@@ -1,7 +1,8 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'print.dart';
typedef InstallConfigCallBack = void Function(String url);
@@ -15,11 +16,11 @@ class LinkManager {
}
initAppLinksListen(installConfigCallBack) async {
debugPrint("initAppLinksListen");
commonPrint.log("initAppLinksListen");
destroy();
subscription = _appLinks.uriLinkStream.listen(
(uri) {
debugPrint('onAppLink: $uri');
commonPrint.log('onAppLink: $uri');
if (uri.host == 'install-config') {
final parameters = uri.queryParameters;
final url = parameters['url'];

View File

@@ -2,9 +2,9 @@ import 'dart:collection';
class FixedList<T> {
final int maxLength;
final List<T> _list = [];
final List<T> _list;
FixedList(this.maxLength);
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
add(T item) {
if (_list.length == maxLength) {
@@ -13,15 +13,26 @@ class FixedList<T> {
_list.add(item);
}
clear() {
_list.clear();
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
FixedList<T> copyWith() {
return FixedList(
maxLength,
list: _list,
);
}
}
class FixedMap<K, V> {
final int maxSize;
int maxSize;
final Map<K, V> _map = {};
final Queue<K> _queue = Queue<K>();
@@ -34,15 +45,21 @@ class FixedMap<K, V> {
}
_map[key] = value;
_queue.add(key);
return value;
}
clear(){
clear() {
_map.clear();
_queue.clear();
}
updateMaxSize(int size){
maxSize = size;
}
V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length;

View File

@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
class Measure {
final TextScaler _textScale;
late BuildContext context;
final BuildContext context;
Measure.of(this.context)
: _textScale = TextScaler.linear(
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
textScaleFactor,
);
Size computeTextSize(
@@ -16,7 +16,10 @@ class Measure {
double maxWidth = double.infinity,
}) {
final textPainter = TextPainter(
text: TextSpan(text: text.data, style: text.style),
text: TextSpan(
text: text.data,
style: text.style,
),
maxLines: text.maxLines,
textScaler: _textScale,
textDirection: text.textDirection ?? TextDirection.ltr,

53
lib/common/mixin.dart Normal file
View File

@@ -0,0 +1,53 @@
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart';
import 'context.dart';
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
set value(T value) {
state = value;
}
@override
bool updateShouldNotify(previous, next) {
final res = super.updateShouldNotify(previous, next);
if (res) {
onUpdate(next);
}
return res;
}
onUpdate(T value) {}
}
mixin PageMixin<T extends StatefulWidget> on State<T> {
void onPageShow() {
initPageState();
}
initPageState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
commonScaffoldState?.updateSearchState(
(_) => onSearch != null
? AppBarSearchState(
onSearch: onSearch!,
)
: null,
);
});
}
void onPageHidden() {}
List<Widget> get actions => [];
Widget? get floatingActionButton => null;
Function(String)? get onSearch => null;
Function(List<String>)? get onKeywordsUpdate => null;
}

View File

@@ -6,55 +6,82 @@ import 'package:flutter/material.dart';
class Navigation {
static Navigation? _instance;
getItems({
List<NavigationItem> getItems({
bool openLogs = false,
bool hasProxies = false,
}) {
return [
const NavigationItem(
icon: Icon(Icons.space_dashboard),
label: "dashboard",
fragment: DashboardFragment(),
label: PageLabel.dashboard,
keep: false,
fragment: DashboardFragment(
key: GlobalObjectKey(PageLabel.dashboard),
),
),
NavigationItem(
icon: const Icon(Icons.rocket),
label: "proxies",
fragment: const ProxiesFragment(),
icon: const Icon(Icons.article),
label: PageLabel.proxies,
fragment: const ProxiesFragment(
key: GlobalObjectKey(
PageLabel.proxies,
),
),
modes: hasProxies
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
: [],
),
const NavigationItem(
icon: Icon(Icons.folder),
label: "profiles",
fragment: ProfilesFragment(),
label: PageLabel.profiles,
fragment: ProfilesFragment(
key: GlobalObjectKey(
PageLabel.profiles,
),
),
),
const NavigationItem(
icon: Icon(Icons.view_timeline),
label: "requests",
fragment: RequestsFragment(),
label: PageLabel.requests,
fragment: RequestsFragment(
key: GlobalObjectKey(
PageLabel.requests,
),
),
description: "requestsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
const NavigationItem(
icon: Icon(Icons.ballot),
label: "connections",
fragment: ConnectionsFragment(),
label: PageLabel.connections,
fragment: ConnectionsFragment(
key: GlobalObjectKey(
PageLabel.connections,
),
),
description: "connectionsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
const NavigationItem(
icon: Icon(Icons.storage),
label: "resources",
label: PageLabel.resources,
description: "resourcesDesc",
keep: false,
fragment: Resources(),
fragment: Resources(
key: GlobalObjectKey(
PageLabel.resources,
),
),
modes: [NavigationItemMode.more],
),
NavigationItem(
icon: const Icon(Icons.adb),
label: "logs",
fragment: const LogsFragment(),
label: PageLabel.logs,
fragment: const LogsFragment(
key: GlobalObjectKey(
PageLabel.logs,
),
),
description: "logsDesc",
modes: openLogs
? [NavigationItemMode.desktop, NavigationItemMode.more]
@@ -62,8 +89,12 @@ class Navigation {
),
const NavigationItem(
icon: Icon(Icons.construction),
label: "tools",
fragment: ToolsFragment(),
label: PageLabel.tools,
fragment: ToolsFragment(
key: GlobalObjectKey(
PageLabel.tools,
),
),
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
),
];

View File

@@ -1,10 +1,12 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async {
if (!globalState.appController.isMobileView) {
if (globalState.appState.viewMode != ViewMode.mobile) {
return await Navigator.of(context).push<T>(
CommonDesktopRoute(
builder: (context) => child,
@@ -68,7 +70,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
Duration get transitionDuration => const Duration(milliseconds: 500);
@override
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
Duration get reverseTransitionDuration => const Duration(milliseconds: 500);
}
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
@@ -192,7 +194,7 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
_primaryPositionCurve = CurvedAnimation(
parent: widget.primaryRouteAnimation,
curve: Curves.fastEaseInToSlowEaseOut,
reverseCurve: Curves.easeInOut,
reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped,
);
_secondaryPositionCurve = CurvedAnimation(
parent: widget.secondaryRouteAnimation,
@@ -216,9 +218,8 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
begin: const _CommonEdgeShadowDecoration(),
end: _CommonEdgeShadowDecoration(
<Color>[
widget.context.colorScheme.inverseSurface.withOpacity(
0.06,
),
widget.context.colorScheme.inverseSurface
.withValues(alpha: 0.02),
Colors.transparent,
],
),
@@ -272,7 +273,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
return;
}
final double shadowWidth = 0.05 * configuration.size!.width;
final double shadowWidth = 1 * configuration.size!.width;
final double shadowHeight = configuration.size!.height;
final double bandWidth = shadowWidth / (colors.length - 1);

View File

@@ -2,8 +2,15 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
extension NumExt on num {
String fixed({digit = 2}) {
return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
String fixed({decimals = 2}) {
String formatted = toStringAsFixed(decimals);
if (formatted.contains('.')) {
formatted = formatted.replaceAll(RegExp(r'0*$'), '');
if (formatted.endsWith('.')) {
formatted = formatted.substring(0, formatted.length - 1);
}
}
return formatted;
}
}

View File

@@ -241,11 +241,6 @@ class Other {
return "${appName}_${DateTime.now().show}.log";
}
Size getScreenSize() {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
Future<String?> getLocalIpAddress() async {
List<NetworkInterface> interfaces = await NetworkInterface.list(
includeLoopback: false,

View File

@@ -4,18 +4,19 @@ import 'dart:convert';
import 'package:fl_clash/models/models.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'constant.dart';
class Preferences {
static Preferences? _instance;
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
Future<bool> get isInit async => await sharedPreferencesCompleter.future != null;
Future<bool> get isInit async =>
await sharedPreferencesCompleter.future != null;
Preferences._internal() {
SharedPreferences.getInstance().then((value) => sharedPreferencesCompleter.complete(value))
.onError((_,__)=>sharedPreferencesCompleter.complete(null));
SharedPreferences.getInstance()
.then((value) => sharedPreferencesCompleter.complete(value))
.onError((_, __) => sharedPreferencesCompleter.complete(null));
}
factory Preferences() {
@@ -23,7 +24,6 @@ class Preferences {
return _instance!;
}
Future<ClashConfig?> getClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
final clashConfigString = preferences?.getString(clashConfigKey);
@@ -32,29 +32,26 @@ class Preferences {
return ClashConfig.fromJson(clashConfigMap);
}
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.setString(
clashConfigKey,
json.encode(clashConfig),
);
return true;
}
Future<Config?> getConfig() async {
final preferences = await sharedPreferencesCompleter.future;
final configString = preferences?.getString(configKey);
if (configString == null) return null;
final configMap = json.decode(configString);
return Config.fromJson(configMap);
return Config.compatibleFromJson(configMap);
}
Future<bool> saveConfig(Config config) async {
final preferences = await sharedPreferencesCompleter.future;
return await preferences?.setString(
configKey,
json.encode(config),
) ?? false;
configKey,
json.encode(config),
) ??
false;
}
clearClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.remove(clashConfigKey);
}
clearPreferences() async {

31
lib/common/print.dart Normal file
View File

@@ -0,0 +1,31 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart';
class CommonPrint {
static CommonPrint? _instance;
CommonPrint._internal();
factory CommonPrint() {
_instance ??= CommonPrint._internal();
return _instance!;
}
log(String? text) {
final payload = "[FlClash] $text";
debugPrint(payload);
if (globalState.isService) {
return;
}
globalState.appController.addLog(
Log(
logLevel: LogLevel.info,
payload: payload,
),
);
}
}
final commonPrint = CommonPrint();

View File

@@ -1,5 +1,5 @@
import 'package:fl_clash/common/common.dart';
import 'package:flutter/cupertino.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/scheduler.dart';
class Render {
@@ -22,26 +22,26 @@ class Render {
}
pause() {
debouncer.call(
"render_pause",
throttler.call(
DebounceTag.renderPause,
_pause,
duration: Duration(seconds: 5),
);
}
resume() {
debouncer.cancel("render_pause");
throttler.cancel(DebounceTag.renderPause);
_resume();
}
void _pause() {
void _pause() async {
if (_isPaused) return;
_isPaused = true;
_beginFrame = _dispatcher.onBeginFrame;
_drawFrame = _dispatcher.onDrawFrame;
_dispatcher.onBeginFrame = null;
_dispatcher.onDrawFrame = null;
debugPrint("[App] pause");
commonPrint.log("pause");
}
void _resume() {
@@ -50,7 +50,7 @@ class Render {
_dispatcher.onBeginFrame = _beginFrame;
_dispatcher.onDrawFrame = _drawFrame;
_dispatcher.scheduleFrame();
debugPrint("[App] resume");
commonPrint.log("resume");
}
}

View File

@@ -3,7 +3,7 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:dio/io.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -11,33 +11,35 @@ import 'package:flutter/cupertino.dart';
class Request {
late final Dio _dio;
late final Dio _clashDio;
String? userAgent;
Request() {
_dio = Dio();
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
return handler.next(options); // 继续请求
_dio = Dio(
BaseOptions(
headers: {
"User-Agent": browserUa,
},
),
);
_clashDio = Dio();
_clashDio.httpClientAdapter = IOHttpClientAdapter(createHttpClient: () {
final client = HttpClient();
client.findProxy = (Uri uri) {
client.userAgent = globalState.ua;
return FlClashHttpOverrides.handleFindProxy(uri);
};
return client;
});
}
Future<Response> getFileResponseForUrl(String url) async {
final response = await _dio
.get(
url,
options: Options(
headers: {
"User-Agent": globalState.appController.clashConfig.globalUa
},
responseType: ResponseType.bytes,
),
)
.timeout(
httpTimeoutDuration * 6,
);
final response = await _clashDio.get(
url,
options: Options(
responseType: ResponseType.bytes,
),
);
return response;
}
@@ -71,31 +73,38 @@ class Request {
return data;
}
final List<String> _ipInfoSources = [
"https://ipwho.is/?fields=ip&output=csv",
"https://ipinfo.io/ip",
"https://ifconfig.me/ip/",
];
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources) {
for (final source in _ipInfoSources.entries) {
try {
final response = await _dio
.get<String>(
source,
final response = await Dio()
.get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
options: Options(
responseType: ResponseType.json,
),
)
.timeout(httpTimeoutDuration);
.timeout(
Duration(
seconds: 30,
),
);
if (response.statusCode != 200 || response.data == null) {
continue;
}
final ipInfo = await clashCore.getCountryCode(response.data!);
if (ipInfo == null && source != _ipInfoSources.last) {
if (response.data == null) {
continue;
}
return ipInfo;
return source.value(response.data!);
} catch (e) {
debugPrint("checkIp error ===> $e");
commonPrint.log("checkIp error ===> $e");
if (e is DioException && e.type == DioExceptionType.cancel) {
throw "cancelled";
}

View File

@@ -2,6 +2,7 @@ import 'dart:math';
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/widgets/scroll.dart';
import 'package:flutter/material.dart';
class BaseScrollBehavior extends MaterialScrollBehavior {
@@ -16,8 +17,6 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
};
}
class BaseScrollBehavior2 extends ScrollBehavior {}
class HiddenBarScrollBehavior extends BaseScrollBehavior {
@override
Widget buildScrollbar(
@@ -36,8 +35,7 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
Widget child,
ScrollableDetails details,
) {
return Scrollbar(
interactive: true,
return CommonAutoHiddenScrollBar(
controller: details.controller,
child: child,
);
@@ -45,6 +43,13 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
}
class NextClampingScrollPhysics extends ClampingScrollPhysics {
const NextClampingScrollPhysics({super.parent});
@override
NextClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return NextClampingScrollPhysics(parent: buildParent(ancestor));
}
@override
Simulation? createBallisticSimulation(
ScrollMetrics position, double velocity) {
@@ -115,9 +120,7 @@ class ReverseScrollPosition extends ScrollPositionWithSingleContext {
super.keepScrollOffset,
super.oldPosition,
super.debugLabel,
}) : _initialPixels = initialPixels ?? 0;
final double _initialPixels;
});
bool _isInit = false;

0
lib/common/state.dart Normal file
View File

View File

@@ -1,7 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'print.dart';
extension StringExtension on String {
bool get isUrl {
@@ -43,8 +43,17 @@ extension StringExtension on String {
RegExp(this);
return true;
} catch (e) {
debugPrint(e.toString());
commonPrint.log(e.toString());
return false;
}
}
}
extension StringExtensionSafe on String? {
String getSafeValue(String defaultValue) {
if (this == null || this!.isEmpty) {
return defaultValue;
}
return this!;
}
}

View File

@@ -46,7 +46,7 @@ class System {
} else if (Platform.isLinux) {
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
final output = result.stdout.trim();
if (output.startsWith('root:') && output.contains('rwx')) {
if (output.startsWith('root:') && output.contains('rws')) {
return true;
}
return false;

View File

@@ -1,15 +1,20 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'color.dart';
extension TextStyleExtension on TextStyle {
TextStyle get toLight => copyWith(color: color?.toLight);
TextStyle get toLight => copyWith(color: color?.opacity80);
TextStyle get toLighter => copyWith(color: color?.toLighter);
TextStyle get toLighter => copyWith(color: color?.opacity60);
TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500);
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
TextStyle get toJetBrainsMono => copyWith(
fontFamily: FontFamily.jetBrainsMono.value,
);
TextStyle adjustSize(int size) => copyWith(
fontSize: fontSize! + size,
);

39
lib/common/theme.dart Normal file
View File

@@ -0,0 +1,39 @@
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
class CommonTheme {
final BuildContext context;
final Map<String, Color> _colorMap;
CommonTheme.of(this.context) : _colorMap = {};
Color get darkenSecondaryContainer {
return _colorMap.getCacheValue(
"darkenSecondaryContainer",
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.1),
);
}
Color get darkenSecondaryContainerLighter {
return _colorMap.getCacheValue(
"darkenSecondaryContainerLighter",
context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1)
.opacity60,
);
}
Color get darken2SecondaryContainer {
return _colorMap.getCacheValue(
"darken2SecondaryContainer",
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.2),
);
}
Color get darken3PrimaryContainer {
return _colorMap.getCacheValue(
"darken3PrimaryContainer",
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3),
);
}
}

View File

@@ -39,10 +39,7 @@ class Tray {
}
update({
required AppState appState,
required AppFlowingState appFlowingState,
required Config config,
required ClashConfig clashConfig,
required TrayState trayState,
bool focus = false,
}) async {
if (Platform.isAndroid) {
@@ -50,7 +47,7 @@ class Tray {
}
if (!Platform.isLinux) {
await _updateSystemTray(
brightness: appState.brightness,
brightness: trayState.brightness,
force: focus,
);
}
@@ -63,9 +60,7 @@ class Tray {
);
menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox(
label: appFlowingState.isStart
? appLocalizations.stop
: appLocalizations.start,
label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
onClick: (_) async {
globalState.appController.updateStart();
},
@@ -80,23 +75,22 @@ class Tray {
onClick: (_) {
globalState.appController.changeMode(mode);
},
checked: mode == clashConfig.mode,
checked: mode == trayState.mode,
),
);
}
menuItems.add(MenuItem.separator());
if (!Platform.isWindows) {
final groups = appState.currentGroups;
for (final group in groups) {
for (final group in trayState.groups) {
List<MenuItem> subMenuItems = [];
for (final proxy in group.all) {
subMenuItems.add(
MenuItem.checkbox(
label: proxy.name,
checked: appState.selectedMap[group.name] == proxy.name,
checked: trayState.selectedMap[group.name] == proxy.name,
onClick: (_) {
final appController = globalState.appController;
appController.config.updateCurrentSelectedMap(
appController.updateCurrentSelectedMap(
group.name,
proxy.name,
);
@@ -117,18 +111,18 @@ class Tray {
),
);
}
if (groups.isNotEmpty) {
if (trayState.groups.isNotEmpty) {
menuItems.add(MenuItem.separator());
}
}
if (appFlowingState.isStart) {
if (trayState.isStart) {
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.tun,
onClick: (_) {
globalState.appController.updateTun();
},
checked: clashConfig.tun.enable,
checked: trayState.tunEnable,
),
);
menuItems.add(
@@ -137,7 +131,7 @@ class Tray {
onClick: (_) {
globalState.appController.updateSystemProxy();
},
checked: config.networkProps.systemProxy,
checked: trayState.systemProxy,
),
);
menuItems.add(MenuItem.separator());
@@ -147,12 +141,12 @@ class Tray {
onClick: (_) async {
globalState.appController.updateAutoLaunch();
},
checked: config.appSetting.autoLaunch,
checked: trayState.autoLaunch,
);
final copyEnvVarMenuItem = MenuItem(
label: appLocalizations.copyEnvVar,
onClick: (_) async {
await _copyEnv(clashConfig.mixedPort);
await _copyEnv(trayState.port);
},
);
menuItems.add(autoStartMenuItem);
@@ -169,12 +163,25 @@ class Tray {
await trayManager.setContextMenu(menu);
if (Platform.isLinux) {
await _updateSystemTray(
brightness: appState.brightness,
brightness: trayState.brightness,
force: focus,
);
}
}
updateTrayTitle([Traffic? traffic]) async {
// if (!Platform.isMacOS) {
// return;
// }
// if (traffic == null) {
// await trayManager.setTitle("");
// } else {
// await trayManager.setTitle(
// "${traffic.up.shortShow} ↑ \n${traffic.down.shortShow} ↓",
// );
// }
}
Future<void> _copyEnv(int port) async {
final url = "http://127.0.0.1:$port";

View File

@@ -1,20 +0,0 @@
import 'package:flutter/material.dart';
import 'context.dart';
mixin ViewMixin<T extends StatefulWidget> on State<T> {
List<Widget> get actions => [];
Widget? get floatingActionButton => null;
initViewState() {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onSearch = onSearch;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
}
Function(String)? get onSearch => null;
Function(List<String>)? get onKeywordsUpdate => null;
}

View File

@@ -1,13 +1,14 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:screen_retriever/screen_retriever.dart';
import 'package:window_manager/window_manager.dart';
class Window {
init(WindowProps props, int version) async {
init(int version) async {
final props = globalState.config.windowProps;
final acquire = await singleInstanceLock.acquire();
if (!acquire) {
exit(0);
@@ -20,10 +21,12 @@ class Window {
await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions(
size: Size(props.width, props.height),
minimumSize: const Size(380, 500),
minimumSize: const Size(380, 400),
);
if (!Platform.isMacOS || version > 10) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
if (!Platform.isMacOS) {
final left = props.left ?? 0;
final top = props.top ?? 0;
final right = left + props.width;
@@ -60,14 +63,16 @@ class Window {
}
show() async {
render?.resume();
await windowManager.show();
await windowManager.focus();
await windowManager.setSkipTaskbar(false);
render?.resume();
}
Future<bool> isVisible() async {
return await windowManager.isVisible();
Future<bool> get isVisible async {
final value = await windowManager.isVisible();
commonPrint.log("window visible check: $value");
return value;
}
close() async {
@@ -75,9 +80,9 @@ class Window {
}
hide() async {
render?.pause();
await windowManager.hide();
await windowManager.setSkipTaskbar(true);
render?.pause();
}
}

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/cupertino.dart';
import 'package:path/path.dart';
class Windows {
@@ -54,7 +53,7 @@ class Windows {
calloc.free(argumentsPtr);
calloc.free(operationPtr);
debugPrint("[Windows] runas: $command $arguments resultCode:$result");
commonPrint.log("windows runas: $command $arguments resultCode:$result");
if (result < 42) {
return false;

View File

@@ -8,31 +8,31 @@ import 'package:archive/archive.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'common/common.dart';
import 'fragments/profiles/override_profile.dart';
import 'models/models.dart';
class AppController {
final BuildContext context;
late AppState appState;
late AppFlowingState appFlowingState;
late Config config;
late ClashConfig clashConfig;
bool lastTunEnable = false;
int? lastProfileModified;
AppController(this.context) {
appState = context.read<AppState>();
config = context.read<Config>();
clashConfig = context.read<ClashConfig>();
appFlowingState = context.read<AppFlowingState>();
}
final BuildContext context;
final WidgetRef _ref;
AppController(this.context, WidgetRef ref) : _ref = ref;
updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
debouncer.call(DebounceTag.updateClashConfig, () {
updateClashConfig(true);
});
}
updateGroupsDebounce() {
@@ -41,14 +41,16 @@ class AppController {
addCheckIpNumDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () {
appState.checkIpNum++;
_ref.read(checkIpNumProvider.notifier).add();
});
}
applyProfileDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () {
applyProfile(isPrue: true);
});
applyProfileDebounce({
bool silence = false,
}) {
debouncer.call(DebounceTag.applyProfile, (silence) {
applyProfile(silence: silence);
}, args: [silence]);
}
savePreferencesDebounce() {
@@ -67,11 +69,12 @@ class AppController {
}
restartCore() async {
await globalState.restartCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
await clashService?.reStart();
await initCore();
if (_ref.read(runTimeProvider.notifier).isStart) {
await globalState.handleStart();
}
}
updateStatus(bool isStart) async {
@@ -81,13 +84,12 @@ class AppController {
updateTraffic,
]);
final currentLastModified =
await config.getCurrentProfile()?.profileLastModified;
if (currentLastModified == null ||
globalState.lastProfileModified == null) {
await _ref.read(currentProfileProvider)?.profileLastModified;
if (currentLastModified == null || lastProfileModified == null) {
addCheckIpNumDebounce();
return;
}
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) {
if (currentLastModified <= (lastProfileModified ?? 0)) {
addCheckIpNumDebounce();
return;
}
@@ -95,9 +97,10 @@ class AppController {
} else {
await globalState.handleStop();
await clashCore.resetTraffic();
appFlowingState.traffics = [];
appFlowingState.totalTraffic = Traffic();
appFlowingState.runTime = null;
_ref.read(trafficsProvider.notifier).clear();
_ref.read(totalTrafficProvider.notifier).value = Traffic();
_ref.read(runTimeProvider.notifier).value = null;
// tray.updateTrayTitle(null);
addCheckIpNumDebounce();
}
}
@@ -107,107 +110,215 @@ class AppController {
if (startTime != null) {
final startTimeStamp = startTime.millisecondsSinceEpoch;
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
_ref.read(runTimeProvider.notifier).value = nowTimeStamp - startTimeStamp;
} else {
appFlowingState.runTime = null;
_ref.read(runTimeProvider.notifier).value = null;
}
}
updateTraffic() {
globalState.updateTraffic(
config: config,
appFlowingState: appFlowingState,
);
updateTraffic() async {
final traffic = await clashCore.getTraffic();
_ref.read(trafficsProvider.notifier).addTraffic(traffic);
_ref.read(totalTrafficProvider.notifier).value =
await clashCore.getTotalTraffic();
}
addProfile(Profile profile) async {
config.setProfile(profile);
if (config.currentProfileId != null) return;
await changeProfile(profile.id);
_ref.read(profilesProvider.notifier).setProfile(profile);
if (_ref.read(currentProfileIdProvider) != null) return;
_ref.read(currentProfileIdProvider.notifier).value = profile.id;
}
deleteProfile(String id) async {
config.deleteProfileById(id);
_ref.read(profilesProvider.notifier).deleteProfileById(id);
clearEffect(id);
if (config.currentProfileId == id) {
if (config.profiles.isNotEmpty) {
final updateId = config.profiles.first.id;
changeProfile(updateId);
if (globalState.config.currentProfileId == id) {
final profiles = globalState.config.profiles;
final currentProfileId = _ref.read(currentProfileIdProvider.notifier);
if (profiles.isNotEmpty) {
final updateId = profiles.first.id;
currentProfileId.value = updateId;
} else {
changeProfile(null);
currentProfileId.value = null;
updateStatus(false);
}
}
}
updateProviders() async {
await globalState.updateProviders(appState);
_ref.read(providersProvider.notifier).value =
await clashCore.getExternalProviders();
}
updateLocalIp() async {
appFlowingState.localIp = null;
_ref.read(localIpProvider.notifier).value = null;
await Future.delayed(commonDuration);
appFlowingState.localIp = await other.getLocalIpAddress();
_ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress();
}
Future<void> updateProfile(Profile profile) async {
final newProfile = await profile.update();
config.setProfile(
newProfile.copyWith(isUpdating: false),
);
if (profile.id == config.currentProfile?.id) {
applyProfileDebounce();
_ref
.read(profilesProvider.notifier)
.setProfile(newProfile.copyWith(isUpdating: false));
if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce(silence: true);
}
}
setProfile(Profile profile) {
config.setProfile(profile);
if (profile.id == config.currentProfile?.id) {
applyProfileDebounce();
_ref.read(profilesProvider.notifier).setProfile(profile);
}
setProfileAndAutoApply(Profile profile) {
_ref.read(profilesProvider.notifier).setProfile(profile);
if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce(silence: true);
}
}
Future<void> updateClashConfig({bool isPatch = true}) async {
setProfiles(List<Profile> profiles) {
_ref.read(profilesProvider.notifier).value = profiles;
}
addLog(Log log) {
_ref.read(logsProvider).add(log);
}
updateOrAddHotKeyAction(HotKeyAction hotKeyAction) {
final hotKeyActions = _ref.read(hotKeyActionsProvider);
final index =
hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action);
if (index == -1) {
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
..add(hotKeyAction);
} else {
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
..[index] = hotKeyAction;
}
_ref.read(hotKeyActionsProvider.notifier).value = index == -1
? (List.from(hotKeyActions)..add(hotKeyAction))
: (List.from(hotKeyActions)..[index] = hotKeyAction);
}
List<Group> getCurrentGroups() {
return _ref.read(currentGroupsStateProvider.select((state) => state.value));
}
String getRealTestUrl(String? url) {
return _ref.read(getRealTestUrlProvider(url));
}
int getProxiesColumns() {
return _ref.read(getProxiesColumnsProvider);
}
addSortNum() {
return _ref.read(sortNumProvider.notifier).add();
}
getCurrentGroupName() {
final currentGroupName = _ref.read(currentProfileProvider.select(
(state) => state?.currentGroupName,
));
return currentGroupName;
}
ProxyCardState getProxyCardState(proxyName) {
return _ref.read(getProxyCardStateProvider(proxyName));
}
getSelectedProxyName(groupName) {
return _ref.read(getSelectedProxyNameProvider(groupName));
}
updateCurrentGroupName(String groupName) {
final profile = _ref.read(currentProfileProvider);
if (profile == null || profile.currentGroupName == groupName) {
return;
}
setProfile(
profile.copyWith(currentGroupName: groupName),
);
}
Future<void> updateClashConfig([bool? isPatch]) async {
commonPrint.log("update clash patch: ${isPatch ?? false}");
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await globalState.updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
await _updateClashConfig(
isPatch,
);
});
}
Future applyProfile({bool isPrue = false}) async {
if (isPrue) {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
Future<void> _updateClashConfig([bool? isPatch]) async {
final profile = _ref.watch(currentProfileProvider);
await _ref.read(currentProfileProvider)?.checkAndUpdate();
final patchConfig = _ref.read(patchClashConfigProvider);
final appSetting = _ref.read(appSettingProvider);
bool enableTun = patchConfig.tun.enable;
if (enableTun != lastTunEnable &&
lastTunEnable == false &&
!Platform.isAndroid) {
final code = await system.authorizeCore();
switch (code) {
case AuthorizeCode.none:
break;
case AuthorizeCode.success:
lastTunEnable = enableTun;
await restartCore();
return;
case AuthorizeCode.error:
enableTun = false;
}
}
if (appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
}
final res = await clashCore.updateConfig(
globalState.getUpdateConfigParams(isPatch),
);
if (res.isNotEmpty) throw res;
lastTunEnable = enableTun;
lastProfileModified = await profile?.profileLastModified;
}
Future _applyProfile() async {
await clashCore.requestGc();
await updateClashConfig();
await updateGroups();
await updateProviders();
}
Future applyProfile({bool silence = false}) async {
if (silence) {
await _applyProfile();
} else {
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
await _applyProfile();
});
}
addCheckIpNumDebounce();
}
changeProfile(String? value) async {
if (value == config.currentProfileId) return;
config.currentProfileId = value;
handleChangeProfile() {
_ref.read(delayDataSourceProvider.notifier).value = {};
applyProfile();
}
updateBrightness(Brightness brightness) {
_ref.read(appBrightnessProvider.notifier).value = brightness;
}
autoUpdateProfiles() async {
for (final profile in config.profiles) {
for (final profile in _ref.read(profilesProvider)) {
if (!profile.autoUpdate) continue;
final isNotNeedUpdate = profile.lastUpdateDate
?.add(
@@ -218,20 +329,29 @@ class AppController {
continue;
}
try {
updateProfile(profile);
await updateProfile(profile);
} catch (e) {
appFlowingState.addLog(
Log(
logLevel: LogLevel.info,
payload: e.toString(),
),
);
_ref.read(logsProvider.notifier).addLog(
Log(
logLevel: LogLevel.info,
payload: e.toString(),
),
);
}
}
}
Future<void> updateGroups() async {
_ref.read(groupsProvider.notifier).value = await retry(
task: () async {
return await clashCore.getProxiesGroups();
},
retryIf: (res) => res.isEmpty,
);
}
updateProfiles() async {
for (final profile in config.profiles) {
for (final profile in _ref.read(profilesProvider)) {
if (profile.type == ProfileType.file) {
continue;
}
@@ -239,34 +359,33 @@ class AppController {
}
}
Future<void> updateGroups() async {
await globalState.updateGroups(appState);
}
updateSystemColorSchemes(SystemColorSchemes systemColorSchemes) {
appState.systemColorSchemes = systemColorSchemes;
updateSystemColorSchemes(ColorSchemes colorSchemes) {
_ref.read(appSchemesProvider.notifier).value = colorSchemes;
}
savePreferences() async {
debugPrint("[APP] savePreferences");
await preferences.saveConfig(config);
await preferences.saveClashConfig(clashConfig);
commonPrint.log("save preferences");
await preferences.saveConfig(globalState.config);
}
changeProxy({
required String groupName,
required String proxyName,
}) async {
await globalState.changeProxy(
config: config,
groupName: groupName,
proxyName: proxyName,
await clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
),
);
if (_ref.read(appSettingProvider).closeConnections) {
clashCore.closeConnections();
}
addCheckIpNumDebounce();
}
handleBackOrExit() async {
if (config.appSetting.minimizeOnExit) {
if (_ref.read(appSettingProvider).minimizeOnExit) {
if (system.isDesktop) {
await savePreferencesDebounce();
}
@@ -289,7 +408,7 @@ class AppController {
}
autoCheckUpdate() async {
if (!config.appSetting.autoCheckUpdate) return;
if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
final res = await request.checkForUpdate();
checkUpdateResultHandle(data: res);
}
@@ -298,6 +417,9 @@ class AppController {
Map<String, dynamic>? data,
bool handleError = false,
}) async {
if(globalState.isPre){
return;
}
if (data != null) {
final tagName = data['tag_name'];
final body = data['body'];
@@ -346,7 +468,7 @@ class AppController {
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.cacheCorrupt),
);
if (res) {
if (res == true) {
final file = File(await appPath.sharedPreferencesPath);
final isExists = await file.exists();
if (isExists) {
@@ -356,33 +478,43 @@ class AppController {
await handleExit();
}
Future<void> initCore() async {
final isInit = await clashCore.isInit;
if (!isInit) {
await clashCore.setState(
globalState.getCoreState(),
);
await clashCore.init();
}
await applyProfile();
}
init() async {
await _handlePreference();
await _handlerDisclaimer();
await globalState.initCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
await initCore();
await _initStatus();
updateTray(true);
autoLaunch?.updateStatus(
config.appSetting.autoLaunch,
_ref.read(appSettingProvider).autoLaunch,
);
autoUpdateProfiles();
autoCheckUpdate();
if (!config.appSetting.silentLaunch) {
if (!_ref.read(appSettingProvider).silentLaunch) {
window?.show();
} else {
window?.hide();
}
_ref.read(initProvider.notifier).value = true;
}
_initStatus() async {
if (Platform.isAndroid) {
await globalState.updateStartTime();
}
final status =
globalState.isStart == true ? true : config.appSetting.autoRun;
final status = globalState.isStart == true
? true
: _ref.read(appSettingProvider).autoRun;
await updateStatus(status);
if (!status) {
@@ -391,35 +523,15 @@ class AppController {
}
setDelay(Delay delay) {
appState.setDelay(delay);
_ref.read(delayDataSourceProvider.notifier).setDelay(delay);
}
toPage(
int index, {
bool hasAnimate = false,
}) {
if (index > appState.currentNavigationItems.length - 1) {
return;
}
appState.currentLabel = appState.currentNavigationItems[index].label;
if ((config.appSetting.isAnimateToPage || hasAnimate)) {
globalState.pageController?.animateToPage(
index,
duration: kTabScrollDuration,
curve: Curves.easeOut,
);
} else {
globalState.pageController?.jumpToPage(index);
}
toPage(PageLabel pageLabel) {
_ref.read(currentPageLabelProvider.notifier).value = pageLabel;
}
toProfiles() {
final index = appState.currentNavigationItems.indexWhere(
(element) => element.label == "profiles",
);
if (index != -1) {
toPage(index);
}
toPage(PageLabel.profiles);
}
initLink() {
@@ -456,17 +568,8 @@ class AppController {
Future<bool> showDisclaimer() async {
return await globalState.showCommonDialog<bool>(
dismissible: false,
child: AlertDialog(
title: Text(appLocalizations.disclaimer),
content: Container(
width: dialogCommonWidth,
constraints: const BoxConstraints(maxHeight: 200),
child: SingleChildScrollView(
child: SelectableText(
appLocalizations.disclaimerDesc,
),
),
),
child: CommonDialog(
title: appLocalizations.disclaimer,
actions: [
TextButton(
onPressed: () {
@@ -476,21 +579,24 @@ class AppController {
),
TextButton(
onPressed: () {
config.appSetting = config.appSetting.copyWith(
disclaimerAccepted: true,
);
_ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(disclaimerAccepted: true),
);
Navigator.of(context).pop<bool>(true);
},
child: Text(appLocalizations.agree),
)
],
child: SelectableText(
appLocalizations.disclaimerDesc,
),
),
) ??
false;
}
_handlerDisclaimer() async {
if (config.appSetting.disclaimerAccepted) {
if (_ref.read(appSettingProvider).disclaimerAccepted) {
return;
}
final isDisclaimerAccepted = await showDisclaimer();
@@ -549,22 +655,14 @@ class AppController {
addProfileFormURL(url);
}
updateViewWidth(double width) {
updateViewSize(Size size) {
WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width;
_ref.read(viewSizeProvider.notifier).value = size;
});
}
int? getDelay(String proxyName, [String? url]) {
final currentDelayMap = appState.delayMap[getRealTestUrl(url)];
return currentDelayMap?[appState.getRealProxyName(proxyName)];
}
String getRealTestUrl(String? url) {
if (url == null || url.isEmpty) {
return config.appSetting.testUrl;
}
return url;
setProvider(ExternalProvider? provider) {
_ref.read(providersProvider.notifier).setProvider(provider);
}
List<Proxy> _sortOfName(List<Proxy> proxies) {
@@ -577,12 +675,17 @@ class AppController {
);
}
List<Proxy> _sortOfDelay(String url, List<Proxy> proxies) {
List<Proxy> _sortOfDelay({
required List<Proxy> proxies,
String? testUrl,
}) {
return List.of(proxies)
..sort(
(a, b) {
final aDelay = getDelay(a.name, url);
final bDelay = getDelay(b.name, url);
final aDelay =
_ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl));
final bDelay =
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl));
if (aDelay == null && bDelay == null) {
return 0;
}
@@ -598,69 +701,102 @@ class AppController {
}
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
return switch (config.proxiesStyle.sortType) {
return switch (_ref.read(proxiesStyleSettingProvider).sortType) {
ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay(getRealTestUrl(url), proxies),
ProxiesSortType.delay => _sortOfDelay(
proxies: proxies,
testUrl: url,
),
ProxiesSortType.name => _sortOfName(proxies),
};
}
String getCurrentSelectedName(String groupName) {
final group = appState.getGroupWithName(groupName);
return group?.getCurrentSelectedName(
config.currentSelectedMap[groupName] ?? '') ??
'';
}
clearEffect(String profileId) async {
final profilePath = await appPath.getProfilePath(profileId);
final providersPath = await appPath.getProvidersPath(profileId);
return await Isolate.run(() async {
if (profilePath != null) {
await File(profilePath).delete(recursive: true);
final profileFile = File(profilePath);
final isExists = await profileFile.exists();
if (isExists) {
profileFile.delete(recursive: true);
}
}
if (providersPath != null) {
await File(providersPath).delete(recursive: true);
final providersFileDir = File(providersPath);
final isExists = await providersFileDir.exists();
if (isExists) {
providersFileDir.delete(recursive: true);
}
}
});
}
bool get isMobileView {
return appState.viewMode == ViewMode.mobile;
}
updateTun() {
clashConfig.tun = clashConfig.tun.copyWith(
enable: !clashConfig.tun.enable,
);
_ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(enable: !state.tun.enable),
);
}
updateSystemProxy() {
config.networkProps = config.networkProps.copyWith(
systemProxy: !config.networkProps.systemProxy,
);
_ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
systemProxy: !state.systemProxy,
),
);
}
updateStart() {
updateStatus(!appFlowingState.isStart);
updateStatus(!_ref.read(runTimeProvider.notifier).isStart);
}
updateCurrentSelectedMap(String groupName, String proxyName) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile != null &&
currentProfile.selectedMap[groupName] != proxyName) {
final SelectedMap selectedMap = Map.from(
currentProfile.selectedMap,
)..[groupName] = proxyName;
_ref.read(profilesProvider.notifier).setProfile(
currentProfile.copyWith(
selectedMap: selectedMap,
),
);
}
}
updateCurrentUnfoldSet(Set<String> value) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile == null) {
return;
}
_ref.read(profilesProvider.notifier).setProfile(
currentProfile.copyWith(
unfoldSet: value,
),
);
}
changeMode(Mode mode) {
clashConfig.mode = mode;
_ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(mode: mode),
);
if (mode == Mode.global) {
config.updateCurrentGroupName(GroupName.GLOBAL.name);
updateCurrentGroupName(GroupName.GLOBAL.name);
}
addCheckIpNumDebounce();
}
updateAutoLaunch() {
config.appSetting = config.appSetting.copyWith(
autoLaunch: !config.appSetting.autoLaunch,
);
_ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoLaunch: !state.autoLaunch,
),
);
}
updateVisible() async {
final visible = await window?.isVisible();
final visible = await window?.isVisible;
if (visible != null && !visible) {
window?.show();
} else {
@@ -669,18 +805,56 @@ class AppController {
}
updateMode() {
final index = Mode.values.indexWhere((item) => item == clashConfig.mode);
if (index == -1) {
_ref.read(patchClashConfigProvider.notifier).updateState(
(state) {
final index = Mode.values.indexWhere((item) => item == state.mode);
if (index == -1) {
return null;
}
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
return state.copyWith(
mode: Mode.values[nextIndex],
);
},
);
}
handleAddOrUpdate(WidgetRef ref, [Rule? rule]) async {
final res = await globalState.showCommonDialog<Rule>(
child: AddRuleDialog(
rule: rule,
snippet: ref.read(
profileOverrideStateProvider.select(
(state) => state.snippet!,
),
),
),
);
if (res == null) {
return;
}
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
clashConfig.mode = Mode.values[nextIndex];
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) {
final model = state.copyWith.overrideData!(
rule: state.overrideData!.rule.updateRules(
(rules) {
final index = rules.indexWhere((item) => item.id == res.id);
if (index == -1) {
return List.from([res, ...rules]);
}
return List.from(rules)..[index] = res;
},
),
);
return model;
},
);
}
Future<bool> exportLogs() async {
final logsRaw = appFlowingState.logs.map(
(item) => item.toString(),
);
final logsRaw = _ref.read(logsProvider).list.map(
(item) => item.toString(),
);
final data = await Isolate.run<List<int>>(() async {
final logsRawString = logsRaw.join("\n");
return utf8.encode(logsRawString);
@@ -695,12 +869,10 @@ class AppController {
Future<List<int>> backupData() async {
final homeDirPath = await appPath.homeDirPath;
final profilesPath = await appPath.profilesPath;
final configJson = config.toJson();
final clashConfigJson = clashConfig.toJson();
final configJson = globalState.config.toJson();
return Isolate.run<List<int>>(() async {
final archive = Archive();
archive.add("config.json", configJson);
archive.add("clashConfig.json", clashConfigJson);
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
final zipEncoder = ZipEncoder();
return zipEncoder.encode(archive) ?? [];
@@ -709,11 +881,7 @@ class AppController {
updateTray([bool focus = false]) async {
tray.update(
appState: appState,
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
focus: focus,
trayState: _ref.read(trayStateProvider),
);
}
@@ -732,32 +900,64 @@ class AppController {
archive.files.where((item) => !item.name.endsWith(".json"));
final configIndex =
configs.indexWhere((config) => config.name == "config.json");
final clashConfigIndex =
configs.indexWhere((config) => config.name == "clashConfig.json");
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
if (configIndex == -1) throw "invalid backup file";
final configFile = configs[configIndex];
final clashConfigFile = configs[clashConfigIndex];
final tempConfig = Config.fromJson(
var tempConfig = Config.compatibleFromJson(
json.decode(
utf8.decode(configFile.content),
),
);
final tempClashConfig = ClashConfig.fromJson(
json.decode(
utf8.decode(clashConfigFile.content),
),
);
for (final profile in profiles) {
final filePath = join(homeDirPath, profile.name);
final file = File(filePath);
await file.create(recursive: true);
await file.writeAsBytes(profile.content);
}
if (recoveryOption == RecoveryOption.onlyProfiles) {
config.update(tempConfig, RecoveryOption.onlyProfiles);
} else {
config.update(tempConfig, RecoveryOption.all);
clashConfig.update(tempClashConfig);
final clashConfigIndex =
configs.indexWhere((config) => config.name == "clashConfig.json");
if (clashConfigIndex != -1) {
final clashConfigFile = configs[clashConfigIndex];
tempConfig = tempConfig.copyWith(
patchClashConfig: ClashConfig.fromJson(
json.decode(
utf8.decode(
clashConfigFile.content,
),
),
),
);
}
_recovery(
tempConfig,
recoveryOption,
);
}
_recovery(Config config, RecoveryOption recoveryOption) {
final profiles = config.profiles;
for (final profile in profiles) {
_ref.read(profilesProvider.notifier).setProfile(profile);
}
final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles;
if (onlyProfiles) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile != null) {
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id;
}
return;
}
_ref.read(patchClashConfigProvider.notifier).value =
config.patchClashConfig;
_ref.read(appSettingProvider.notifier).value = config.appSetting;
_ref.read(currentProfileIdProvider.notifier).value =
config.currentProfileId;
_ref.read(appDAVSettingProvider.notifier).value = config.dav;
_ref.read(themeSettingProvider.notifier).value = config.themeProps;
_ref.read(windowSettingProvider.notifier).value = config.windowProps;
_ref.read(vpnSettingProvider.notifier).value = config.vpnProps;
_ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle;
_ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
_ref.read(networkSettingProvider.notifier).value = config.networkProps;
_ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
}
}

View File

@@ -34,7 +34,24 @@ const desktopPlatforms = [
SupportPlatform.Windows,
];
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
enum GroupType {
Selector,
URLTest,
Fallback,
LoadBalance,
Relay;
static GroupType parseProfileType(String type) {
return switch (type) {
"url-test" => URLTest,
"select" => Selector,
"fallback" => Fallback,
"load-balance" => LoadBalance,
"relay" => Relay,
String() => throw UnimplementedError(),
};
}
}
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
@@ -45,7 +62,7 @@ extension GroupTypeExtension on GroupType {
)
.toList();
bool get isURLTestOrFallback {
bool get isComputedSelected {
return [GroupType.URLTest, GroupType.Fallback].contains(this);
}
@@ -138,6 +155,13 @@ enum DnsMode {
hosts
}
enum ExternalControllerStatus {
@JsonValue("")
close,
@JsonValue("127.0.0.1:9090")
open
}
enum KeyboardModifier {
alt([
PhysicalKeyboardKey.altLeft,
@@ -195,14 +219,13 @@ enum ProxiesIconStyle {
}
enum FontFamily {
system(),
miSans("MiSans"),
twEmoji("Twemoji"),
jetBrainsMono("JetBrainsMono"),
icon("Icons");
final String? value;
final String value;
const FontFamily([this.value]);
const FontFamily(this.value);
}
enum RouteMode {
@@ -238,6 +261,7 @@ enum ActionMethod {
stopListener,
getCountryCode,
getMemory,
getProfile,
///Android,
setFdMap,
@@ -270,7 +294,11 @@ enum DebounceTag {
handleWill,
updateDelay,
vpnTip,
autoLaunch
autoLaunch,
renderPause,
updatePageIndex,
pageChange,
proxiesTabChange,
}
enum DashboardWidget {
@@ -341,3 +369,81 @@ enum DashboardWidget {
return dashboardWidgets[index];
}
}
enum GeodataLoader {
standard,
memconservative,
}
enum PageLabel {
dashboard,
proxies,
profiles,
tools,
logs,
requests,
resources,
connections,
}
enum RuleAction {
DOMAIN("DOMAIN"),
DOMAIN_SUFFIX("DOMAIN-SUFFIX"),
DOMAIN_KEYWORD("DOMAIN-KEYWORD"),
DOMAIN_REGEX("DOMAIN-REGEX"),
GEOSITE("GEOSITE"),
IP_CIDR("IP-CIDR"),
IP_CIDR6("IP-CIDR6"),
IP_SUFFIX("IP-SUFFIX"),
IP_ASN("IP-ASN"),
GEOIP("GEOIP"),
SRC_GEOIP("SRC-GEOIP"),
SRC_IP_ASN("SRC-IP-ASN"),
SRC_IP_CIDR("SRC-IP-CIDR"),
SRC_IP_SUFFIX("SRC-IP-SUFFIX"),
DST_PORT("DST-PORT"),
SRC_PORT("SRC-PORT"),
IN_PORT("IN-PORT"),
IN_TYPE("IN-TYPE"),
IN_USER("IN-USER"),
IN_NAME("IN-NAME"),
PROCESS_PATH("PROCESS-PATH"),
PROCESS_PATH_REGEX("PROCESS-PATH-REGEX"),
PROCESS_NAME("PROCESS-NAME"),
PROCESS_NAME_REGEX("PROCESS-NAME-REGEX"),
UID("UID"),
NETWORK("NETWORK"),
DSCP("DSCP"),
RULE_SET("RULE-SET"),
AND("AND"),
OR("OR"),
NOT("NOT"),
SUB_RULE("SUB-RULE"),
MATCH("MATCH");
final String value;
const RuleAction(this.value);
}
extension RuleActionExt on RuleAction {
bool get hasParams => [
RuleAction.GEOIP,
RuleAction.IP_ASN,
RuleAction.SRC_IP_ASN,
RuleAction.IP_CIDR,
RuleAction.IP_CIDR6,
RuleAction.IP_SUFFIX,
RuleAction.RULE_SET,
].contains(this);
}
enum OverrideRuleType {
override,
added,
}
enum RuleTarget {
DIRECT,
REJECT,
}

View File

@@ -4,20 +4,21 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AccessFragment extends StatefulWidget {
class AccessFragment extends ConsumerStatefulWidget {
const AccessFragment({super.key});
@override
State<AccessFragment> createState() => _AccessFragmentState();
ConsumerState<AccessFragment> createState() => _AccessFragmentState();
}
class _AccessFragmentState extends State<AccessFragment> {
class _AccessFragmentState extends ConsumerState<AccessFragment> {
List<String> acceptList = [];
List<String> rejectList = [];
late ScrollController _controller;
@@ -28,10 +29,11 @@ class _AccessFragmentState extends State<AccessFragment> {
_updateInitList();
_controller = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
final appState = globalState.appState;
if (appState.packages.isEmpty) {
Future.delayed(const Duration(milliseconds: 300), () async {
appState.packages = await app?.getPackages() ?? [];
ref.read(packagesProvider.notifier).value =
await app?.getPackages() ?? [];
});
}
});
@@ -44,9 +46,8 @@ class _AccessFragmentState extends State<AccessFragment> {
}
_updateInitList() {
final accessControl = globalState.appController.config.accessControl;
acceptList = accessControl.acceptList;
rejectList = accessControl.rejectList;
acceptList = globalState.config.vpnProps.accessControl.acceptList;
rejectList = globalState.config.vpnProps.accessControl.rejectList;
}
Widget _buildSearchButton() {
@@ -59,9 +60,13 @@ class _AccessFragmentState extends State<AccessFragment> {
acceptList: acceptList,
rejectList: rejectList,
),
).then((_) => setState(() {
).then(
(_) => setState(
() {
_updateInitList();
}));
},
),
);
},
icon: const Icon(Icons.search),
);
@@ -77,28 +82,29 @@ class _AccessFragmentState extends State<AccessFragment> {
return IconButton(
tooltip: tooltip,
onPressed: () {
final config = globalState.appController.config;
final isAccept =
config.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: [],
),
false => config.accessControl.copyWith(
rejectList: [],
),
};
} else {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: allValueList,
),
false => config.accessControl.copyWith(
rejectList: allValueList,
),
};
}
ref.read(vpnSettingProvider.notifier).updateState((state) {
final isAccept =
state.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) {
return switch (isAccept) {
true => state.copyWith.accessControl(
acceptList: [],
),
false => state.copyWith.accessControl(
rejectList: [],
),
};
} else {
return switch (isAccept) {
true => state.copyWith.accessControl(
acceptList: allValueList,
),
false => state.copyWith.accessControl(
rejectList: allValueList,
),
};
}
});
},
icon: isSelectedAll
? const Icon(Icons.deselect)
@@ -106,223 +112,247 @@ class _AccessFragmentState extends State<AccessFragment> {
);
}
Widget _buildSettingButton() {
return IconButton(
onPressed: () {
showSheet(
title: appLocalizations.proxiesSetting,
context: context,
body: AccessControlWidget(
context: context,
_intelligentSelected() async {
final appState = globalState.appState;
final config = globalState.config;
final accessControl = config.vpnProps.accessControl;
final packageNames = appState.packages
.where(
(item) =>
accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
(await commonScaffoldState?.loadingRun<List<String>>(
() async {
return await app?.getChinaPackageNames() ?? [];
},
))
?.toSet() ??
{};
final acceptList = packageNames
.where((item) => !selectedPackageNames.contains(item))
.toList();
final rejectList = packageNames
.where((item) => selectedPackageNames.contains(item))
.toList();
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
acceptList: acceptList,
rejectList: rejectList,
),
);
}
Widget _buildSettingButton() {
return IconButton(
onPressed: () async {
final res = await showSheet<int>(
context: context,
props: SheetProps(
isScrollControlled: true,
),
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: AccessControlPanel(),
title: appLocalizations.proxiesSetting,
);
},
);
if (res == 1) {
_intelligentSelected();
}
},
icon: const Icon(Icons.tune),
);
}
_handleSelected(List<String> valueList, Package package, bool? value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
ref.read(vpnSettingProvider.notifier).updateState((state) {
return switch (
state.accessControl.mode == AccessControlMode.acceptSelected) {
true => state.copyWith.accessControl(
acceptList: valueList,
),
false => state.copyWith.accessControl(
rejectList: valueList,
),
};
});
}
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.isAccessControl,
builder: (_, isAccessControl, child) {
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 0,
child: ListItem.switchItem(
title: Text(appLocalizations.appAccessControl),
delegate: SwitchDelegate(
value: isAccessControl,
onChanged: (isAccessControl) {
final config = context.read<Config>();
config.isAccessControl = isAccessControl;
},
),
),
final state = ref.watch(packageListSelectorStateProvider);
final accessControl = state.accessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
);
final currentList = accessControl.currentList;
final packageNameList = packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe = accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 0,
child: ListItem.switchItem(
title: Text(appLocalizations.appAccessControl),
delegate: SwitchDelegate(
value: accessControl.enable,
onChanged: (enable) {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
enable: enable,
),
);
},
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Divider(
height: 12,
),
),
Flexible(
child: child!,
),
],
);
},
child: Selector<AppState, List<Package>>(
selector: (_, appState) => appState.packages,
builder: (_, packages, ___) {
return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
packages: appState.packages,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
);
final currentList = accessControl.currentList;
final packageNameList =
packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe =
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return DisabledMask(
status: !isAccessControl,
child: Column(
children: [
ActivateBox(
active: isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Divider(
height: 12,
),
),
Flexible(
child: DisabledMask(
status: !accessControl.enable,
child: Column(
children: [
ActivateBox(
active: accessControl.enable,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
bottom: 4,
left: 16,
right: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: _buildSearchButton(),
),
Flexible(
child: _buildSelectedAllButton(
isSelectedAll: valueList.length ==
packageNameList.length,
allValueList: packageNameList,
Expanded(
child: Row(
children: [
Flexible(
child: Text(
appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
const Flexible(
child: SizedBox(
width: 8,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: _buildSettingButton(),
),
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSearchButton(),
),
Flexible(
child: _buildSelectedAllButton(
isSelectedAll:
valueList.length == packageNameList.length,
allValueList: packageNameList,
),
),
Flexible(
child: _buildSettingButton(),
),
],
),
),
],
),
Expanded(
flex: 1,
child: packages.isEmpty
? const Center(
child: CircularProgressIndicator(),
)
: CommonScrollBar(
controller: _controller,
child: ListView.builder(
controller: _controller,
itemCount: packages.length,
itemExtent: 72,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value:
valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config =
globalState.appController.config;
if (accessControlMode ==
AccessControlMode.acceptSelected) {
config.accessControl =
config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl =
config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
),
),
),
],
),
),
);
},
);
},
),
Expanded(
flex: 1,
child: packages.isEmpty
? const Center(
child: CircularProgressIndicator(),
)
: CommonScrollBar(
controller: _controller,
child: ListView.builder(
controller: _controller,
itemCount: packages.length,
itemExtent: 72,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value: valueList.contains(package.packageName),
isActive: accessControl.enable,
onChanged: (value) {
_handleSelected(valueList, package, value);
},
);
},
),
),
),
],
),
),
),
],
);
}
}
@@ -343,45 +373,47 @@ class PackageListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ActivateBox(
active: isActive,
child: ListItem.checkbox(
leading: SizedBox(
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: app?.getPackageIcon(package.packageName),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
return FadeScaleEnterBox(
child: ActivateBox(
active: isActive,
child: ListItem.checkbox(
leading: SizedBox(
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: app?.getPackageIcon(package.packageName),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
),
),
title: Text(
package.label,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
title: Text(
package.label,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
),
maxLines: 1,
),
subtitle: Text(
package.packageName,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
subtitle: Text(
package.packageName,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
),
delegate: CheckboxDelegate(
value: value,
onChanged: onChanged,
),
maxLines: 1,
),
delegate: CheckboxDelegate(
value: value,
onChanged: onChanged,
),
),
);
@@ -426,15 +458,31 @@ class AccessControlSearchDelegate extends SearchDelegate {
);
}
_handleSelected(
WidgetRef ref, List<String> valueList, Package package, bool? value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
ref.read(vpnSettingProvider.notifier).updateState((state) {
return switch (
state.accessControl.mode == AccessControlMode.acceptSelected) {
true => state.copyWith.accessControl(
acceptList: valueList,
),
false => state.copyWith.accessControl(
rejectList: valueList,
),
};
});
}
Widget _packageList() {
final lowQuery = query.toLowerCase();
return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
packages: appState.packages,
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
return Consumer(
builder: (context, ref, __) {
final state = ref.watch(packageListSelectorStateProvider);
final accessControl = state.accessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
@@ -449,7 +497,7 @@ class AccessControlSearchDelegate extends SearchDelegate {
package.packageName.contains(lowQuery),
)
.toList();
final isAccessControl = state.isAccessControl;
final isAccessControl = state.accessControl.enable;
final currentList = accessControl.currentList;
final packageNameList = packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
@@ -465,21 +513,12 @@ class AccessControlSearchDelegate extends SearchDelegate {
value: valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config = globalState.appController.config;
if (accessControlMode == AccessControlMode.acceptSelected) {
config.accessControl = config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl = config.accessControl.copyWith(
rejectList: valueList,
);
}
_handleSelected(
ref,
valueList,
package,
value,
);
},
);
},
@@ -500,14 +539,16 @@ class AccessControlSearchDelegate extends SearchDelegate {
}
}
class AccessControlWidget extends StatelessWidget {
final BuildContext context;
const AccessControlWidget({
class AccessControlPanel extends ConsumerStatefulWidget {
const AccessControlPanel({
super.key,
required this.context,
});
@override
ConsumerState createState() => _AccessControlPanelState();
}
class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
return switch (mode) {
AccessControlMode.acceptSelected => Icons.adjust_outlined,
@@ -552,9 +593,11 @@ class AccessControlWidget extends StatelessWidget {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, AccessControlMode>(
selector: (_, config) => config.accessControl.mode,
builder: (_, accessControlMode, __) {
child: Consumer(
builder: (_, ref, __) {
final accessControlMode = ref.watch(
vpnSettingProvider.select((state) => state.accessControl.mode),
);
return Wrap(
spacing: 16,
children: [
@@ -566,10 +609,11 @@ class AccessControlWidget extends StatelessWidget {
),
isSelected: accessControlMode == item,
onPressed: () {
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
mode: item,
);
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
mode: item,
),
);
},
)
],
@@ -588,9 +632,11 @@ class AccessControlWidget extends StatelessWidget {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, AccessSortType>(
selector: (_, config) => config.accessControl.sort,
builder: (_, accessSortType, __) {
child: Consumer(
builder: (_, ref, __) {
final accessSortType = ref.watch(
vpnSettingProvider.select((state) => state.accessControl.sort),
);
return Wrap(
spacing: 16,
children: [
@@ -602,10 +648,11 @@ class AccessControlWidget extends StatelessWidget {
),
isSelected: accessSortType == item,
onPressed: () {
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
sort: item,
);
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
sort: item,
),
);
},
),
],
@@ -624,9 +671,12 @@ class AccessControlWidget extends StatelessWidget {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, bool>(
selector: (_, config) => config.accessControl.isFilterSystemApp,
builder: (_, isFilterSystemApp, __) {
child: Consumer(
builder: (_, ref, __) {
final isFilterSystemApp = ref.watch(
vpnSettingProvider
.select((state) => state.accessControl.isFilterSystemApp),
);
return Wrap(
spacing: 16,
children: [
@@ -635,10 +685,11 @@ class AccessControlWidget extends StatelessWidget {
_getTextWithIsFilterSystemApp(item),
isSelected: isFilterSystemApp == item,
onPressed: () {
final config = globalState.appController.config;
config.accessControl = config.accessControl.copyWith(
isFilterSystemApp: item,
);
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
isFilterSystemApp: item,
),
);
},
)
],
@@ -650,63 +701,35 @@ class AccessControlWidget extends StatelessWidget {
);
}
_intelligentSelected() async {
final appState = globalState.appController.appState;
final config = globalState.appController.config;
final accessControl = config.accessControl;
final packageNames = appState.packages
.where(
(item) =>
accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
Navigator.of(context).pop();
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
(await commonScaffoldState?.loadingRun<List<String>>(
() async {
return await app?.getChinaPackageNames() ?? [];
},
))
?.toSet() ??
{};
final acceptList = packageNames
.where((item) => !selectedPackageNames.contains(item))
.toList();
final rejectList = packageNames
.where((item) => selectedPackageNames.contains(item))
.toList();
config.accessControl = accessControl.copyWith(
acceptList: acceptList,
rejectList: rejectList,
);
}
_copyToClipboard() async {
await globalState.safeRun(() {
final data = globalState.appController.config.accessControl.toJson();
final data = globalState.config.vpnProps.accessControl.toJson();
Clipboard.setData(
ClipboardData(
text: json.encode(data),
),
);
});
if (!context.mounted) return;
if (!mounted) return;
Navigator.of(context).pop();
}
_pasteToClipboard() async {
await globalState.safeRun(() async {
final config = globalState.appController.config;
final data = await Clipboard.getData('text/plain');
final text = data?.text;
if (text == null) return;
config.accessControl = AccessControl.fromJson(
json.decode(text),
);
});
if (!context.mounted) return;
await globalState.safeRun(
() async {
final data = await Clipboard.getData('text/plain');
final text = data?.text;
if (text == null) return;
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
accessControl: AccessControl.fromJson(
json.decode(text),
),
),
);
},
);
if (!mounted) return;
Navigator.of(context).pop();
}
@@ -725,7 +748,9 @@ class AccessControlWidget extends StatelessWidget {
CommonChip(
avatar: const Icon(Icons.auto_awesome),
label: appLocalizations.intelligentSelected,
onPressed: _intelligentSelected,
onPressed: () {
Navigator.of(context).pop(1);
},
),
CommonChip(
avatar: const Icon(Icons.paste),
@@ -746,17 +771,19 @@ class AccessControlWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildModeSetting(),
..._buildSortSetting(),
..._buildSourceSetting(),
..._buildActionSetting(),
],
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildModeSetting(),
..._buildSortSetting(),
..._buildSourceSetting(),
..._buildActionSetting(),
],
),
),
);
}

View File

@@ -1,61 +1,258 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class CloseConnectionsSwitch extends StatelessWidget {
const CloseConnectionsSwitch({super.key});
class CloseConnectionsItem extends ConsumerWidget {
const CloseConnectionsItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.closeConnections,
builder: (_, closeConnections, __) {
return ListItem.switchItem(
title: Text(appLocalizations.autoCloseConnections),
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
delegate: SwitchDelegate(
value: closeConnections,
onChanged: (value) async {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
closeConnections: value,
Widget build(BuildContext context, ref) {
final closeConnections = ref.watch(
appSettingProvider.select((state) => state.closeConnections),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoCloseConnections),
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
delegate: SwitchDelegate(
value: closeConnections,
onChanged: (value) async {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
closeConnections: value,
),
);
},
),
);
},
},
),
);
}
}
class UsageSwitch extends StatelessWidget {
const UsageSwitch({super.key});
class UsageItem extends ConsumerWidget {
const UsageItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.onlyStatisticsProxy,
builder: (_, onlyProxy, __) {
return ListItem.switchItem(
title: Text(appLocalizations.onlyStatisticsProxy),
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
delegate: SwitchDelegate(
value: onlyProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
onlyStatisticsProxy: value,
Widget build(BuildContext context, ref) {
final onlyStatisticsProxy = ref.watch(
appSettingProvider.select((state) => state.onlyStatisticsProxy),
);
return ListItem.switchItem(
title: Text(appLocalizations.onlyStatisticsProxy),
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
delegate: SwitchDelegate(
value: onlyStatisticsProxy,
onChanged: (bool value) async {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
onlyStatisticsProxy: value,
),
);
},
),
);
},
},
),
);
}
}
class MinimizeItem extends ConsumerWidget {
const MinimizeItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final minimizeOnExit = ref.watch(
appSettingProvider.select((state) => state.minimizeOnExit),
);
return ListItem.switchItem(
title: Text(appLocalizations.minimizeOnExit),
subtitle: Text(appLocalizations.minimizeOnExitDesc),
delegate: SwitchDelegate(
value: minimizeOnExit,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
minimizeOnExit: value,
),
);
},
),
);
}
}
class AutoLaunchItem extends ConsumerWidget {
const AutoLaunchItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoLaunch = ref.watch(
appSettingProvider.select((state) => state.autoLaunch),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoLaunch),
subtitle: Text(appLocalizations.autoLaunchDesc),
delegate: SwitchDelegate(
value: autoLaunch,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoLaunch: value,
),
);
},
),
);
}
}
class SilentLaunchItem extends ConsumerWidget {
const SilentLaunchItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final silentLaunch = ref.watch(
appSettingProvider.select((state) => state.silentLaunch),
);
return ListItem.switchItem(
title: Text(appLocalizations.silentLaunch),
subtitle: Text(appLocalizations.silentLaunchDesc),
delegate: SwitchDelegate(
value: silentLaunch,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
silentLaunch: value,
),
);
},
),
);
}
}
class AutoRunItem extends ConsumerWidget {
const AutoRunItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoRun = ref.watch(
appSettingProvider.select((state) => state.autoRun),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(
value: autoRun,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoRun: value,
),
);
},
),
);
}
}
class HiddenItem extends ConsumerWidget {
const HiddenItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final hidden = ref.watch(
appSettingProvider.select((state) => state.hidden),
);
return ListItem.switchItem(
title: Text(appLocalizations.exclude),
subtitle: Text(appLocalizations.excludeDesc),
delegate: SwitchDelegate(
value: hidden,
onChanged: (value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
hidden: value,
),
);
},
),
);
}
}
class AnimateTabItem extends ConsumerWidget {
const AnimateTabItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isAnimateToPage = ref.watch(
appSettingProvider.select((state) => state.isAnimateToPage),
);
return ListItem.switchItem(
title: Text(appLocalizations.tabAnimation),
subtitle: Text(appLocalizations.tabAnimationDesc),
delegate: SwitchDelegate(
value: isAnimateToPage,
onChanged: (value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
isAnimateToPage: value,
),
);
},
),
);
}
}
class OpenLogsItem extends ConsumerWidget {
const OpenLogsItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final openLogs = ref.watch(
appSettingProvider.select((state) => state.openLogs),
);
return ListItem.switchItem(
title: Text(appLocalizations.logcat),
subtitle: Text(appLocalizations.logcatDesc),
delegate: SwitchDelegate(
value: openLogs,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
openLogs: value,
),
);
},
),
);
}
}
class AutoCheckUpdateItem extends ConsumerWidget {
const AutoCheckUpdateItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoCheckUpdate = ref.watch(
appSettingProvider.select((state) => state.autoCheckUpdate),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoCheckUpdate),
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
delegate: SwitchDelegate(
value: autoCheckUpdate,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoCheckUpdate: value,
),
);
},
),
);
}
}
@@ -71,156 +268,20 @@ class ApplicationSettingFragment extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<Widget> items = [
Selector<Config, bool>(
selector: (_, config) => config.appSetting.minimizeOnExit,
builder: (_, isMinimizeOnExit, child) {
return ListItem.switchItem(
title: Text(appLocalizations.minimizeOnExit),
subtitle: Text(appLocalizations.minimizeOnExitDesc),
delegate: SwitchDelegate(
value: isMinimizeOnExit,
onChanged: (bool value) {
final config = context.read<Config>();
config.appSetting = config.appSetting.copyWith(
minimizeOnExit: value,
);
},
),
);
},
),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoLaunch,
builder: (_, autoLaunch, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoLaunch),
subtitle: Text(appLocalizations.autoLaunchDesc),
delegate: SwitchDelegate(
value: autoLaunch,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoLaunch: value,
);
},
),
);
},
),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.silentLaunch,
builder: (_, silentLaunch, child) {
return ListItem.switchItem(
title: Text(appLocalizations.silentLaunch),
subtitle: Text(appLocalizations.silentLaunchDesc),
delegate: SwitchDelegate(
value: silentLaunch,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
silentLaunch: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoRun,
builder: (_, autoRun, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(
value: autoRun,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoRun: value,
);
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.hidden,
builder: (_, isExclude, child) {
return ListItem.switchItem(
title: Text(appLocalizations.exclude),
subtitle: Text(appLocalizations.excludeDesc),
delegate: SwitchDelegate(
value: isExclude,
onChanged: (value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
hidden: value,
);
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.isAnimateToPage,
builder: (_, isAnimateToPage, child) {
return ListItem.switchItem(
title: Text(appLocalizations.tabAnimation),
subtitle: Text(appLocalizations.tabAnimationDesc),
delegate: SwitchDelegate(
value: isAnimateToPage,
onChanged: (value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
isAnimateToPage: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.openLogs,
builder: (_, openLogs, child) {
return ListItem.switchItem(
title: Text(appLocalizations.logcat),
subtitle: Text(appLocalizations.logcatDesc),
delegate: SwitchDelegate(
value: openLogs,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
openLogs: value,
);
},
),
);
},
),
const CloseConnectionsSwitch(),
const UsageSwitch(),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoCheckUpdate,
builder: (_, autoCheckUpdate, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoCheckUpdate),
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
delegate: SwitchDelegate(
value: autoCheckUpdate,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoCheckUpdate: value,
);
},
),
);
},
),
MinimizeItem(),
if (system.isDesktop) ...[
AutoLaunchItem(),
SilentLaunchItem(),
],
AutoRunItem(),
if (Platform.isAndroid) ...[
HiddenItem(),
],
AnimateTabItem(),
OpenLogsItem(),
CloseConnectionsItem(),
UsageItem(),
AutoCheckUpdateItem(),
];
return ListView.separated(
itemBuilder: (_, index) {

View File

@@ -4,14 +4,16 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/dav_client.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart';
import 'package:fl_clash/widgets/fade_box.dart';
import 'package:fl_clash/widgets/list.dart';
import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class BackupAndRecovery extends StatelessWidget {
class BackupAndRecovery extends ConsumerWidget {
const BackupAndRecovery({super.key});
_showAddWebDAV(DAV? dav) async {
@@ -121,139 +123,140 @@ class BackupAndRecovery extends StatelessWidget {
_recoveryOnLocal(context, recoveryOption);
}
@override
Widget build(BuildContext context) {
return Selector<Config, DAV?>(
selector: (_, config) => config.dav,
builder: (_, dav, __) {
final client = dav != null ? DAVClient(dav) : null;
return ListView(
children: [
ListHeader(title: appLocalizations.remote),
if (dav == null)
ListItem(
leading: const Icon(Icons.account_box),
title: Text(appLocalizations.noInfo),
subtitle: Text(appLocalizations.pleaseBindWebDAV),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.bind,
),
),
)
else ...[
ListItem(
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: client!.pingCompleter.future,
builder: (_, snapshot) {
return Center(
child: FadeBox(
child: snapshot.connectionState ==
ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
const SizedBox(
height: 4,
),
ListItem.input(
title: Text(appLocalizations.file),
subtitle: Text(dav.fileName),
delegate: InputDelegate(
title: appLocalizations.file,
value: dav.fileName,
resetValue: defaultDavFileName,
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.appController.config.dav =
globalState.appController.config.dav?.copyWith(
fileName: value,
);
},
),
),
ListItem(
onTap: () {
_backupOnWebDAV(context, client);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.remoteBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnWebDAV(context, client);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.remoteRecoveryDesc),
),
],
ListHeader(title: appLocalizations.local),
ListItem(
onTap: () {
_backupOnLocal(context);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnLocal(context);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
),
],
_handleChange(String? value, WidgetRef ref) {
if (value == null) {
return;
}
ref.read(appDAVSettingProvider.notifier).updateState(
(state) => state?.copyWith(
fileName: value,
),
);
},
}
@override
Widget build(BuildContext context, ref) {
final dav = ref.watch(appDAVSettingProvider);
final client = dav != null ? DAVClient(dav) : null;
return ListView(
children: [
ListHeader(title: appLocalizations.remote),
if (dav == null)
ListItem(
leading: const Icon(Icons.account_box),
title: Text(appLocalizations.noInfo),
subtitle: Text(appLocalizations.pleaseBindWebDAV),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.bind,
),
),
)
else ...[
ListItem(
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: client!.pingCompleter.future,
builder: (_, snapshot) {
return Center(
child: FadeThroughBox(
child: snapshot.connectionState ==
ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
const SizedBox(
height: 4,
),
ListItem.input(
title: Text(appLocalizations.file),
subtitle: Text(dav.fileName),
delegate: InputDelegate(
title: appLocalizations.file,
value: dav.fileName,
resetValue: defaultDavFileName,
onChanged: (value) {
_handleChange(value, ref);
},
),
),
ListItem(
onTap: () {
_backupOnWebDAV(context, client);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.remoteBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnWebDAV(context, client);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.remoteRecoveryDesc),
),
],
ListHeader(title: appLocalizations.local),
ListItem(
onTap: () {
_backupOnLocal(context);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnLocal(context);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
),
],
);
}
}
@@ -273,45 +276,42 @@ class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.recovery),
contentPadding: const EdgeInsets.symmetric(
return CommonDialog(
title: appLocalizations.recovery,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.onlyProfiles);
},
title: Text(appLocalizations.recoveryProfiles),
),
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.all);
},
title: Text(appLocalizations.recoveryAll),
)
],
),
child: Wrap(
children: [
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.onlyProfiles);
},
title: Text(appLocalizations.recoveryProfiles),
),
ListItem(
onTap: () {
_handleOnTab(RecoveryOption.all);
},
title: Text(appLocalizations.recoveryAll),
)
],
),
);
}
}
class WebDAVFormDialog extends StatefulWidget {
class WebDAVFormDialog extends ConsumerStatefulWidget {
final DAV? dav;
const WebDAVFormDialog({super.key, this.dav});
@override
State<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
ConsumerState<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
}
class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
late TextEditingController uriController;
late TextEditingController userController;
late TextEditingController passwordController;
@@ -328,7 +328,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
_submit() {
if (!_formKey.currentState!.validate()) return;
globalState.appController.config.dav = DAV(
ref.read(appDAVSettingProvider.notifier).value = DAV(
uri: uriController.text,
user: userController.text,
password: passwordController.text,
@@ -337,7 +337,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
}
_delete() {
globalState.appController.config.dav = null;
ref.read(appDAVSettingProvider.notifier).value = null;
Navigator.pop(context);
}
@@ -349,78 +349,8 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.webDAVConfiguration),
content: Form(
key: _formKey,
child: SizedBox(
width: dialogCommonWidth,
child: Wrap(
runSpacing: 16,
children: [
TextFormField(
controller: uriController,
maxLines: 5,
minLines: 1,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link),
border: const OutlineInputBorder(),
labelText: appLocalizations.address,
helperText: appLocalizations.addressHelp,
),
validator: (String? value) {
if (value == null || value.isEmpty || !value.isUrl) {
return appLocalizations.addressTip;
}
return null;
},
),
TextFormField(
controller: userController,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.account_circle),
border: const OutlineInputBorder(),
labelText: appLocalizations.account,
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return appLocalizations.accountTip;
}
return null;
},
),
ValueListenableBuilder(
valueListenable: _obscureController,
builder: (_, obscure, __) {
return TextFormField(
controller: passwordController,
obscureText: obscure,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.password),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
obscure ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
_obscureController.value = !obscure;
},
),
labelText: appLocalizations.password,
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return appLocalizations.passwordTip;
}
return null;
},
);
},
),
],
),
),
),
return CommonDialog(
title: appLocalizations.webDAVConfiguration,
actions: [
if (widget.dav != null)
TextButton(
@@ -432,6 +362,73 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
child: Text(appLocalizations.save),
)
],
child: Form(
key: _formKey,
child: Wrap(
runSpacing: 16,
children: [
TextFormField(
controller: uriController,
maxLines: 5,
minLines: 1,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link),
border: const OutlineInputBorder(),
labelText: appLocalizations.address,
helperText: appLocalizations.addressHelp,
),
validator: (String? value) {
if (value == null || value.isEmpty || !value.isUrl) {
return appLocalizations.addressTip;
}
return null;
},
),
TextFormField(
controller: userController,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.account_circle),
border: const OutlineInputBorder(),
labelText: appLocalizations.account,
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return appLocalizations.accountTip;
}
return null;
},
),
ValueListenableBuilder(
valueListenable: _obscureController,
builder: (_, obscure, __) {
return TextFormField(
controller: passwordController,
obscureText: obscure,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.password),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
obscure ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
_obscureController.value = !obscure;
},
),
labelText: appLocalizations.password,
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return appLocalizations.passwordTip;
}
return null;
},
);
},
),
],
),
),
);
}
}

View File

@@ -2,8 +2,13 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/config/dns.dart';
import 'package:fl_clash/fragments/config/general.dart';
import 'package:fl_clash/fragments/config/network.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/providers/config.dart' show patchClashConfigProvider;
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../state.dart';
class ConfigFragment extends StatefulWidget {
const ConfigFragment({super.key});
@@ -16,18 +21,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
@override
Widget build(BuildContext context) {
List<Widget> items = [
ListItem.open(
title: Text(appLocalizations.network),
subtitle: Text(appLocalizations.networkDesc),
leading: const Icon(Icons.vpn_key),
delegate: OpenDelegate(
title: appLocalizations.network,
isScaffold: true,
isBlur: false,
extendPageWidth: 360,
widget: const NetworkListView(),
),
),
ListItem.open(
title: Text(appLocalizations.general),
subtitle: Text(appLocalizations.generalDesc),
@@ -37,20 +30,52 @@ class _ConfigFragmentState extends State<ConfigFragment> {
widget: generateListView(
generalItems,
),
isBlur: false,
extendPageWidth: 360,
blur: false,
),
),
ListItem.open(
title: Text(appLocalizations.network),
subtitle: Text(appLocalizations.networkDesc),
leading: const Icon(Icons.vpn_key),
delegate: OpenDelegate(
title: appLocalizations.network,
blur: false,
widget: const NetworkListView(),
),
),
ListItem.open(
title: const Text("DNS"),
subtitle: Text(appLocalizations.dnsDesc),
leading: const Icon(Icons.dns),
delegate: const OpenDelegate(
delegate: OpenDelegate(
title: "DNS",
widget: DnsListView(),
isScaffold: true,
isBlur: false,
extendPageWidth: 360,
action: Consumer(builder: (_, ref, __) {
return IconButton(
onPressed: () async {
final res = await globalState.showMessage(
title: appLocalizations.reset,
message: TextSpan(
text: appLocalizations.resetTip,
),
);
if (res != true) {
return;
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
dns: defaultDns,
),
);
},
tooltip: appLocalizations.reset,
icon: const Icon(
Icons.replay,
),
);
}),
widget: const DnsListView(),
blur: false,
),
)
];

View File

@@ -1,208 +1,196 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class OverrideItem extends StatelessWidget {
class OverrideItem extends ConsumerWidget {
const OverrideItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.overrideDns,
builder: (_, override, __) {
return ListItem.switchItem(
title: Text(appLocalizations.overrideDns),
subtitle: Text(appLocalizations.overrideDnsDesc),
delegate: SwitchDelegate(
value: override,
onChanged: (bool value) async {
final config = globalState.appController.config;
config.overrideDns = value;
},
),
);
},
Widget build(BuildContext context, ref) {
final override = ref.watch(overrideDnsProvider);
return ListItem.switchItem(
title: Text(appLocalizations.overrideDns),
subtitle: Text(appLocalizations.overrideDnsDesc),
delegate: SwitchDelegate(
value: override,
onChanged: (bool value) async {
ref.read(overrideDnsProvider.notifier).value = value;
},
),
);
}
}
class StatusItem extends StatelessWidget {
class StatusItem extends ConsumerWidget {
const StatusItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: Text(appLocalizations.status),
subtitle: Text(appLocalizations.statusDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
enable: value,
);
},
),
);
},
Widget build(BuildContext context, ref) {
final enable =
ref.watch(patchClashConfigProvider.select((state) => state.dns.enable));
return ListItem.switchItem(
title: Text(appLocalizations.status),
subtitle: Text(appLocalizations.statusDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(enable: value));
},
),
);
}
}
class PreferH3Item extends StatelessWidget {
class ListenItem extends ConsumerWidget {
const ListenItem({super.key});
@override
Widget build(BuildContext context, ref) {
final listen =
ref.watch(patchClashConfigProvider.select((state) => state.dns.listen));
return ListItem.input(
title: Text(appLocalizations.listen),
subtitle: Text(listen),
delegate: InputDelegate(
title: appLocalizations.listen,
value: listen,
onChanged: (String? value) {
if (value == null) {
return;
}
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(listen: value));
},
),
);
}
}
class PreferH3Item extends ConsumerWidget {
const PreferH3Item({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.preferH3,
builder: (_, preferH3, __) {
return ListItem.switchItem(
title: const Text("PreferH3"),
subtitle: Text(appLocalizations.preferH3Desc),
delegate: SwitchDelegate(
value: preferH3,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
preferH3: value,
);
},
),
);
},
Widget build(BuildContext context, ref) {
final preferH3 = ref
.watch(patchClashConfigProvider.select((state) => state.dns.preferH3));
return ListItem.switchItem(
title: const Text("PreferH3"),
subtitle: Text(appLocalizations.preferH3Desc),
delegate: SwitchDelegate(
value: preferH3,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(preferH3: value));
},
),
);
}
}
class IPv6Item extends StatelessWidget {
class IPv6Item extends ConsumerWidget {
const IPv6Item({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
title: const Text("IPv6"),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
ipv6: value,
);
},
),
);
},
Widget build(BuildContext context, ref) {
final ipv6 = ref.watch(
patchClashConfigProvider.select((state) => state.dns.ipv6),
);
return ListItem.switchItem(
title: const Text("IPv6"),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(ipv6: value));
},
),
);
}
}
class RespectRulesItem extends StatelessWidget {
class RespectRulesItem extends ConsumerWidget {
const RespectRulesItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.respectRules,
builder: (_, respectRules, __) {
return ListItem.switchItem(
title: Text(appLocalizations.respectRules),
subtitle: Text(appLocalizations.respectRulesDesc),
delegate: SwitchDelegate(
value: respectRules,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
respectRules: value,
);
},
),
);
},
Widget build(BuildContext context, ref) {
final respectRules = ref.watch(
patchClashConfigProvider.select((state) => state.dns.respectRules),
);
return ListItem.switchItem(
title: Text(appLocalizations.respectRules),
subtitle: Text(appLocalizations.respectRulesDesc),
delegate: SwitchDelegate(
value: respectRules,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(respectRules: value));
},
),
);
}
}
class DnsModeItem extends StatelessWidget {
class DnsModeItem extends ConsumerWidget {
const DnsModeItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, DnsMode>(
selector: (_, clashConfig) => clashConfig.dns.enhancedMode,
builder: (_, enhancedMode, __) {
return ListItem<DnsMode>.options(
title: Text(appLocalizations.dnsMode),
subtitle: Text(enhancedMode.name),
delegate: OptionsDelegate(
title: appLocalizations.dnsMode,
options: DnsMode.values,
onChanged: (value) {
if (value == null) {
return;
}
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(enhancedMode: value);
},
textBuilder: (dnsMode) => dnsMode.name,
value: enhancedMode,
),
);
},
Widget build(BuildContext context, ref) {
final enhancedMode = ref.watch(
patchClashConfigProvider.select((state) => state.dns.enhancedMode),
);
return ListItem<DnsMode>.options(
title: Text(appLocalizations.dnsMode),
subtitle: Text(enhancedMode.name),
delegate: OptionsDelegate(
title: appLocalizations.dnsMode,
options: DnsMode.values,
onChanged: (value) {
if (value == null) {
return;
}
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(enhancedMode: value));
},
textBuilder: (dnsMode) => dnsMode.name,
value: enhancedMode,
),
);
}
}
class FakeIpRangeItem extends StatelessWidget {
class FakeIpRangeItem extends ConsumerWidget {
const FakeIpRangeItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, String>(
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange,
builder: (_, fakeIpRange, __) {
return ListItem.input(
title: Text(appLocalizations.fakeipRange),
subtitle: Text(fakeIpRange),
delegate: InputDelegate(
title: appLocalizations.fakeipRange,
value: fakeIpRange,
onChanged: (String? value) {
if (value != null) {
try {
final clashConfig = globalState.appController.clashConfig;
clashConfig.dns = clashConfig.dns.copyWith(
fakeIpRange: value,
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.fakeipRange,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
Widget build(BuildContext context, ref) {
final fakeIpRange = ref.watch(
patchClashConfigProvider.select((state) => state.dns.fakeIpRange),
);
return ListItem.input(
title: Text(appLocalizations.fakeipRange),
subtitle: Text(fakeIpRange),
delegate: InputDelegate(
title: appLocalizations.fakeipRange,
value: fakeIpRange,
onChanged: (String? value) {
if (value == null) {
return;
}
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(fakeIpRange: value));
},
),
);
}
}
@@ -215,27 +203,28 @@ class FakeIpFilterItem extends StatelessWidget {
return ListItem.open(
title: Text(appLocalizations.fakeipFilter),
delegate: OpenDelegate(
isBlur: false,
blur: false,
title: appLocalizations.fakeipFilter,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, fakeIpFilter, __) {
return ListPage(
widget: Consumer(
builder: (_, ref, __) {
final fakeIpFilter = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fakeIpFilter),
);
return ListInputPage(
title: appLocalizations.fakeipFilter,
items: fakeIpFilter,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fakeIpFilter: List.from(items),
);
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(
fakeIpFilter: List.from(items),
));
},
);
},
),
extendPageWidth: 360,
),
);
}
@@ -250,27 +239,26 @@ class DefaultNameserverItem extends StatelessWidget {
title: Text(appLocalizations.defaultNameserver),
subtitle: Text(appLocalizations.defaultNameserverDesc),
delegate: OpenDelegate(
isBlur: false,
blur: false,
title: appLocalizations.defaultNameserver,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, defaultNameserver, __) {
return ListPage(
title: appLocalizations.defaultNameserver,
items: defaultNameserver,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
defaultNameserver: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
widget: Consumer(builder: (_, ref, __) {
final defaultNameserver = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.defaultNameserver),
);
return ListInputPage(
title: appLocalizations.defaultNameserver,
items: defaultNameserver,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns(
defaultNameserver: List.from(items),
),
);
},
);
}),
),
);
}
@@ -286,79 +274,71 @@ class NameserverItem extends StatelessWidget {
subtitle: Text(appLocalizations.nameserverDesc),
delegate: OpenDelegate(
title: appLocalizations.nameserver,
isBlur: false,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.nameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, nameserver, __) {
return ListPage(
title: "域名服务器",
items: nameserver,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
nameserver: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
blur: false,
widget: Consumer(builder: (_, ref, __) {
final nameserver = ref.watch(
patchClashConfigProvider.select((state) => state.dns.nameserver),
);
return ListInputPage(
title: appLocalizations.nameserver,
items: nameserver,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns(
nameserver: List.from(items),
),
);
},
);
}),
),
);
}
}
class UseHostsItem extends StatelessWidget {
class UseHostsItem extends ConsumerWidget {
const UseHostsItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.useHosts,
builder: (_, useHosts, __) {
return ListItem.switchItem(
title: Text(appLocalizations.useHosts),
delegate: SwitchDelegate(
value: useHosts,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
useHosts: value,
);
},
),
);
},
Widget build(BuildContext context, ref) {
final useHosts = ref.watch(
patchClashConfigProvider.select((state) => state.dns.useHosts),
);
return ListItem.switchItem(
title: Text(appLocalizations.useHosts),
delegate: SwitchDelegate(
value: useHosts,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(useHosts: value));
},
),
);
}
}
class UseSystemHostsItem extends StatelessWidget {
class UseSystemHostsItem extends ConsumerWidget {
const UseSystemHostsItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts,
builder: (_, useSystemHosts, __) {
return ListItem.switchItem(
title: Text(appLocalizations.useSystemHosts),
delegate: SwitchDelegate(
value: useSystemHosts,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
useSystemHosts: value,
);
},
),
);
},
Widget build(BuildContext context, ref) {
final useSystemHosts = ref.watch(
patchClashConfigProvider.select((state) => state.dns.useSystemHosts),
);
return ListItem.switchItem(
title: Text(appLocalizations.useSystemHosts),
delegate: SwitchDelegate(
value: useSystemHosts,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(
useSystemHosts: value,
));
},
),
);
}
}
@@ -372,29 +352,27 @@ class NameserverPolicyItem extends StatelessWidget {
title: Text(appLocalizations.nameserverPolicy),
subtitle: Text(appLocalizations.nameserverPolicyDesc),
delegate: OpenDelegate(
isBlur: false,
blur: false,
title: appLocalizations.nameserverPolicy,
widget: Selector<ClashConfig, Map<String, String>>(
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
shouldRebuild: (prev, next) =>
!const MapEquality<String, String>().equals(prev, next),
builder: (_, nameserverPolicy, __) {
return ListPage(
title: appLocalizations.nameserverPolicy,
items: nameserverPolicy.entries,
titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
nameserverPolicy: Map.fromEntries(items),
);
},
);
},
),
extendPageWidth: 360,
widget: Consumer(builder: (_, ref, __) {
final nameserverPolicy = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.nameserverPolicy),
);
return MapInputPage(
title: appLocalizations.nameserverPolicy,
map: nameserverPolicy,
titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value),
onChange: (value) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns(
nameserverPolicy: value,
),
);
},
);
}),
),
);
}
@@ -409,27 +387,28 @@ class ProxyServerNameserverItem extends StatelessWidget {
title: Text(appLocalizations.proxyNameserver),
subtitle: Text(appLocalizations.proxyNameserverDesc),
delegate: OpenDelegate(
isBlur: false,
blur: false,
title: appLocalizations.proxyNameserver,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, proxyServerNameserver, __) {
return ListPage(
widget: Consumer(
builder: (_, ref, __) {
final proxyServerNameserver = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.proxyServerNameserver),
);
return ListInputPage(
title: appLocalizations.proxyNameserver,
items: proxyServerNameserver,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
proxyServerNameserver: List.from(items),
);
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns(
proxyServerNameserver: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
@@ -444,95 +423,81 @@ class FallbackItem extends StatelessWidget {
title: Text(appLocalizations.fallback),
subtitle: Text(appLocalizations.fallbackDesc),
delegate: OpenDelegate(
isBlur: false,
blur: false,
title: appLocalizations.fallback,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallback,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, fallback, __) {
return ListPage(
title: appLocalizations.fallback,
items: fallback,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallback: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
widget: Consumer(builder: (_, ref, __) {
final fallback = ref.watch(
patchClashConfigProvider.select((state) => state.dns.fallback),
);
return ListInputPage(
title: appLocalizations.fallback,
items: fallback,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns(
fallback: List.from(items),
),
);
},
);
}),
),
);
}
}
class GeoipItem extends StatelessWidget {
class GeoipItem extends ConsumerWidget {
const GeoipItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip,
builder: (_, geoip, __) {
return ListItem.switchItem(
title: const Text("Geoip"),
delegate: SwitchDelegate(
value: geoip,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value),
);
},
),
);
},
Widget build(BuildContext context, ref) {
final geoip = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.geoip),
);
return ListItem.switchItem(
title: const Text("Geoip"),
delegate: SwitchDelegate(
value: geoip,
onChanged: (bool value) async {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns.fallbackFilter(
geoip: value,
));
},
),
);
}
}
class GeoipCodeItem extends StatelessWidget {
class GeoipCodeItem extends ConsumerWidget {
const GeoipCodeItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, String>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode,
builder: (_, geoipCode, __) {
return ListItem.input(
title: Text(appLocalizations.geoipCode),
subtitle: Text(geoipCode),
delegate: InputDelegate(
title: appLocalizations.geoipCode,
value: geoipCode,
onChanged: (String? value) {
if (value != null) {
try {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
geoipCode: value,
),
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.geoipCode,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
Widget build(BuildContext context, ref) {
final geoipCode = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.geoipCode),
);
return ListItem.input(
title: Text(appLocalizations.geoipCode),
subtitle: Text(geoipCode),
delegate: InputDelegate(
title: appLocalizations.geoipCode,
value: geoipCode,
onChanged: (String? value) {
if (value == null) {
return;
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns.fallbackFilter(
geoipCode: value,
),
);
},
),
);
}
}
@@ -545,29 +510,26 @@ class GeositeItem extends StatelessWidget {
return ListItem.open(
title: const Text("Geosite"),
delegate: OpenDelegate(
isBlur: false,
blur: false,
title: "Geosite",
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, geosite, __) {
return ListPage(
title: "Geosite",
items: geosite,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
geosite: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
widget: Consumer(builder: (_, ref, __) {
final geosite = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.geosite),
);
return ListInputPage(
title: "Geosite",
items: geosite,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns.fallbackFilter(
geosite: List.from(items),
),
);
},
);
}),
),
);
}
@@ -581,29 +543,26 @@ class IpcidrItem extends StatelessWidget {
return ListItem.open(
title: Text(appLocalizations.ipcidr),
delegate: OpenDelegate(
isBlur: false,
blur: false,
title: appLocalizations.ipcidr,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, ipcidr, __) {
return ListPage(
title: appLocalizations.ipcidr,
items: ipcidr,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
ipcidr: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
widget: Consumer(builder: (_, ref, ___) {
final ipcidr = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.ipcidr),
);
return ListInputPage(
title: appLocalizations.ipcidr,
items: ipcidr,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns.fallbackFilter(
ipcidr: List.from(items),
));
},
);
}),
),
);
}
@@ -617,29 +576,26 @@ class DomainItem extends StatelessWidget {
return ListItem.open(
title: Text(appLocalizations.domain),
delegate: OpenDelegate(
isBlur: false,
blur: false,
title: appLocalizations.domain,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, domain, __) {
return ListPage(
title: appLocalizations.domain,
items: domain,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
domain: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
widget: Consumer(builder: (_, ref, __) {
final domain = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.domain),
);
return ListInputPage(
title: appLocalizations.domain,
items: domain,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.dns.fallbackFilter(
domain: List.from(items),
),
);
},
);
}),
),
);
}
@@ -655,6 +611,7 @@ class DnsOptions extends StatelessWidget {
title: appLocalizations.options,
items: [
const StatusItem(),
const ListenItem(),
const UseHostsItem(),
const UseSystemHostsItem(),
const IPv6Item(),
@@ -700,37 +657,11 @@ const dnsItems = <Widget>[
FallbackFilterOptions(),
];
class DnsListView extends StatelessWidget {
class DnsListView extends ConsumerWidget {
const DnsListView({super.key});
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context.commonScaffoldState?.actions = [
IconButton(
onPressed: () async {
final res = await globalState.showMessage(
title: appLocalizations.reset,
message: TextSpan(
text: appLocalizations.resetTip,
),
);
if (res != true) {
return;
}
globalState.appController.clashConfig.dns = defaultDns;
},
tooltip: appLocalizations.reset,
icon: const Icon(
Icons.replay,
),
)
];
});
}
@override
Widget build(BuildContext context) {
_initActions(context);
Widget build(BuildContext context, ref) {
return generateListView(
dnsItems,
);

View File

@@ -1,198 +1,190 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class LogLevelItem extends StatelessWidget {
class LogLevelItem extends ConsumerWidget {
const LogLevelItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem<LogLevel>.options(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(value.name),
delegate: OptionsDelegate<LogLevel>(
title: appLocalizations.logLevel,
options: LogLevel.values,
onChanged: (LogLevel? value) {
if (value == null) {
return;
}
final appController = globalState.appController;
appController.clashConfig.logLevel = value;
},
textBuilder: (logLevel) => logLevel.name,
value: value,
),
);
},
Widget build(BuildContext context, ref) {
final logLevel =
ref.watch(patchClashConfigProvider.select((state) => state.logLevel));
return ListItem<LogLevel>.options(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(logLevel.name),
delegate: OptionsDelegate<LogLevel>(
title: appLocalizations.logLevel,
options: LogLevel.values,
onChanged: (LogLevel? value) {
if (value == null) {
return;
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
logLevel: value,
),
);
},
textBuilder: (logLevel) => logLevel.name,
value: logLevel,
),
);
}
}
class UaItem extends StatelessWidget {
class UaItem extends ConsumerWidget {
const UaItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, String?>(
selector: (_, clashConfig) => clashConfig.globalRealUa,
builder: (_, value, __) {
return ListItem<String?>.options(
leading: const Icon(Icons.computer_outlined),
title: const Text("UA"),
subtitle: Text(value ?? appLocalizations.defaultText),
delegate: OptionsDelegate<String?>(
title: "UA",
options: [
null,
"clash-verge/v1.6.6",
"ClashforWindows/0.19.23",
],
value: value,
onChanged: (ua) {
final appController = globalState.appController;
appController.clashConfig.globalRealUa = ua;
},
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
),
);
},
Widget build(BuildContext context, ref) {
final globalUa =
ref.watch(patchClashConfigProvider.select((state) => state.globalUa));
return ListItem<String?>.options(
leading: const Icon(Icons.computer_outlined),
title: const Text("UA"),
subtitle: Text(globalUa ?? appLocalizations.defaultText),
delegate: OptionsDelegate<String?>(
title: "UA",
options: [
null,
"clash-verge/v1.6.6",
"ClashforWindows/0.19.23",
],
value: globalUa,
onChanged: (value) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
globalUa: value,
),
);
},
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
),
);
}
}
class KeepAliveIntervalItem extends StatelessWidget {
class KeepAliveIntervalItem extends ConsumerWidget {
const KeepAliveIntervalItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, int>(
selector: (_, config) => config.keepAliveInterval,
builder: (_, value, __) {
return ListItem.input(
leading: const Icon(Icons.timer_outlined),
title: Text(appLocalizations.keepAliveIntervalDesc),
subtitle: Text("$value ${appLocalizations.seconds}"),
delegate: InputDelegate(
title: appLocalizations.keepAliveIntervalDesc,
suffixText: appLocalizations.seconds,
resetValue: "$defaultKeepAliveInterval",
value: "$value",
onChanged: (String? value) {
if (value != null) {
try {
final intValue = int.parse(value);
if (intValue <= 0) {
throw "Invalid keepAliveInterval";
}
globalState.appController.clashConfig.keepAliveInterval =
intValue;
} catch (e) {
globalState.showMessage(
title: appLocalizations.keepAliveIntervalDesc,
message: TextSpan(
text: e.toString(),
Widget build(BuildContext context, ref) {
final keepAliveInterval = ref.watch(
patchClashConfigProvider.select((state) => state.keepAliveInterval));
return ListItem.input(
leading: const Icon(Icons.timer_outlined),
title: Text(appLocalizations.keepAliveIntervalDesc),
subtitle: Text("$keepAliveInterval ${appLocalizations.seconds}"),
delegate: InputDelegate(
title: appLocalizations.keepAliveIntervalDesc,
suffixText: appLocalizations.seconds,
resetValue: "$defaultKeepAliveInterval",
value: "$keepAliveInterval",
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.safeRun(
() {
final intValue = int.parse(value);
if (intValue <= 0) {
throw "Invalid keepAliveInterval";
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
keepAliveInterval: intValue,
),
);
}
}
},
),
);
},
silence: false,
title: appLocalizations.keepAliveIntervalDesc,
);
},
),
);
}
}
class TestUrlItem extends StatelessWidget {
class TestUrlItem extends ConsumerWidget {
const TestUrlItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, String>(
selector: (_, config) => config.appSetting.testUrl,
builder: (_, value, __) {
return ListItem.input(
leading: const Icon(Icons.timeline),
title: Text(appLocalizations.testUrl),
subtitle: Text(value),
delegate: InputDelegate(
resetValue: defaultTestUrl,
title: appLocalizations.testUrl,
value: value,
onChanged: (String? value) {
if (value != null) {
try {
if (!value.isUrl) {
throw "Invalid url";
}
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
testUrl: value,
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
);
Widget build(BuildContext context, ref) {
final testUrl =
ref.watch(appSettingProvider.select((state) => state.testUrl));
return ListItem.input(
leading: const Icon(Icons.timeline),
title: Text(appLocalizations.testUrl),
subtitle: Text(testUrl),
delegate: InputDelegate(
resetValue: defaultTestUrl,
title: appLocalizations.testUrl,
value: testUrl,
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.safeRun(
() {
if (!value.isUrl) {
throw "Invalid url";
}
}
},
),
);
},
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
testUrl: value,
),
);
},
silence: false,
title: appLocalizations.testUrl,
);
}),
);
}
}
class MixedPortItem extends StatelessWidget {
class MixedPortItem extends ConsumerWidget {
const MixedPortItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, value, __) {
return ListItem.input(
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text("$value"),
delegate: InputDelegate(
title: appLocalizations.proxyPort,
value: "$value",
onChanged: (String? value) {
if (value != null) {
try {
final mixedPort = int.parse(value);
if (mixedPort < 1024 || mixedPort > 49151) {
throw "Invalid port";
}
globalState.appController.clashConfig.mixedPort = mixedPort;
} catch (e) {
globalState.showMessage(
title: appLocalizations.proxyPort,
message: TextSpan(
text: e.toString(),
Widget build(BuildContext context, ref) {
final mixedPort =
ref.watch(patchClashConfigProvider.select((state) => state.mixedPort));
return ListItem.input(
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text("$mixedPort"),
delegate: InputDelegate(
title: appLocalizations.proxyPort,
value: "$mixedPort",
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.safeRun(
() {
final mixedPort = int.parse(value);
if (mixedPort < 1024 || mixedPort > 49151) {
throw "Invalid port";
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
mixedPort: mixedPort,
),
);
}
}
},
resetValue: "$defaultMixedPort",
),
);
},
silence: false,
title: appLocalizations.proxyPort,
);
},
resetValue: "$defaultMixedPort",
),
);
}
}
@@ -207,216 +199,218 @@ class HostsItem extends StatelessWidget {
title: const Text("Hosts"),
subtitle: Text(appLocalizations.hostsDesc),
delegate: OpenDelegate(
isBlur: false,
blur: false,
title: "Hosts",
widget: Selector<ClashConfig, HostsMap>(
selector: (_, clashConfig) => clashConfig.hosts,
shouldRebuild: (prev, next) =>
!const MapEquality<String, String>().equals(prev, next),
builder: (_, hosts, ___) {
final entries = hosts.entries;
return ListPage(
widget: Consumer(
builder: (_, ref, __) {
final hosts = ref
.watch(patchClashConfigProvider.select((state) => state.hosts));
return MapInputPage(
title: "Hosts",
items: entries,
map: hosts,
titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
clashConfig.hosts = Map.fromEntries(items);
onChange: (value) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
hosts: value,
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class Ipv6Item extends StatelessWidget {
class Ipv6Item extends ConsumerWidget {
const Ipv6Item({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
leading: const Icon(Icons.water_outlined),
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6Desc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.ipv6 = value;
},
),
);
},
Widget build(BuildContext context, ref) {
final ipv6 =
ref.watch(patchClashConfigProvider.select((state) => state.ipv6));
return ListItem.switchItem(
leading: const Icon(Icons.water_outlined),
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6Desc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
ipv6: value,
),
);
},
),
);
}
}
class AllowLanItem extends StatelessWidget {
class AllowLanItem extends ConsumerWidget {
const AllowLanItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.allowLan,
builder: (_, allowLan, __) {
return ListItem.switchItem(
leading: const Icon(Icons.device_hub),
title: Text(appLocalizations.allowLan),
subtitle: Text(appLocalizations.allowLanDesc),
delegate: SwitchDelegate(
value: allowLan,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.allowLan = value;
},
),
);
},
Widget build(BuildContext context, ref) {
final allowLan =
ref.watch(patchClashConfigProvider.select((state) => state.allowLan));
return ListItem.switchItem(
leading: const Icon(Icons.device_hub),
title: Text(appLocalizations.allowLan),
subtitle: Text(appLocalizations.allowLanDesc),
delegate: SwitchDelegate(
value: allowLan,
onChanged: (bool value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
allowLan: value,
),
);
},
),
);
}
}
class UnifiedDelayItem extends StatelessWidget {
class UnifiedDelayItem extends ConsumerWidget {
const UnifiedDelayItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.unifiedDelay,
builder: (_, unifiedDelay, __) {
return ListItem.switchItem(
leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate(
value: unifiedDelay,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.unifiedDelay = value;
},
),
);
},
Widget build(BuildContext context, ref) {
final unifiedDelay = ref
.watch(patchClashConfigProvider.select((state) => state.unifiedDelay));
return ListItem.switchItem(
leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate(
value: unifiedDelay,
onChanged: (bool value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
unifiedDelay: value,
),
);
},
),
);
}
}
class FindProcessItem extends StatelessWidget {
class FindProcessItem extends ConsumerWidget {
const FindProcessItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, findProcess, __) {
return ListItem.switchItem(
leading: const Icon(Icons.polymer_outlined),
title: Text(appLocalizations.findProcessMode),
subtitle: Text(appLocalizations.findProcessModeDesc),
delegate: SwitchDelegate(
value: findProcess,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off;
},
),
);
},
Widget build(BuildContext context, ref) {
final findProcess = ref.watch(patchClashConfigProvider
.select((state) => state.findProcessMode == FindProcessMode.always));
return ListItem.switchItem(
leading: const Icon(Icons.polymer_outlined),
title: Text(appLocalizations.findProcessMode),
subtitle: Text(appLocalizations.findProcessModeDesc),
delegate: SwitchDelegate(
value: findProcess,
onChanged: (bool value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
findProcessMode:
value ? FindProcessMode.always : FindProcessMode.off,
),
);
},
),
);
}
}
class TcpConcurrentItem extends StatelessWidget {
class TcpConcurrentItem extends ConsumerWidget {
const TcpConcurrentItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
builder: (_, tcpConcurrent, __) {
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow_outlined),
title: Text(appLocalizations.tcpConcurrent),
subtitle: Text(appLocalizations.tcpConcurrentDesc),
delegate: SwitchDelegate(
value: tcpConcurrent,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value;
},
),
);
},
Widget build(BuildContext context, ref) {
final tcpConcurrent = ref
.watch(patchClashConfigProvider.select((state) => state.tcpConcurrent));
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow_outlined),
title: Text(appLocalizations.tcpConcurrent),
subtitle: Text(appLocalizations.tcpConcurrentDesc),
delegate: SwitchDelegate(
value: tcpConcurrent,
onChanged: (value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
tcpConcurrent: value,
),
);
},
),
);
}
}
class GeodataLoaderItem extends StatelessWidget {
class GeodataLoaderItem extends ConsumerWidget {
const GeodataLoaderItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.geodataLoader == geodataLoaderMemconservative,
builder: (_, memconservative, __) {
return ListItem.switchItem(
leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate(
value: memconservative,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.geodataLoader =
value ? geodataLoaderMemconservative : geodataLoaderStandard;
},
),
);
},
Widget build(BuildContext context, ref) {
final isMemconservative = ref.watch(patchClashConfigProvider.select(
(state) => state.geodataLoader == GeodataLoader.memconservative));
return ListItem.switchItem(
leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate(
value: isMemconservative,
onChanged: (bool value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
geodataLoader: value
? GeodataLoader.memconservative
: GeodataLoader.standard,
),
);
},
),
);
}
}
class ExternalControllerItem extends StatelessWidget {
class ExternalControllerItem extends ConsumerWidget {
const ExternalControllerItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty,
builder: (_, hasExternalController, __) {
return ListItem.switchItem(
leading: const Icon(Icons.api_outlined),
title: Text(appLocalizations.externalController),
subtitle: Text(appLocalizations.externalControllerDesc),
delegate: SwitchDelegate(
value: hasExternalController,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.externalController =
value ? defaultExternalController : '';
},
),
);
},
Widget build(BuildContext context, ref) {
final hasExternalController = ref.watch(patchClashConfigProvider.select(
(state) => state.externalController == ExternalControllerStatus.open));
return ListItem.switchItem(
leading: const Icon(Icons.api_outlined),
title: Text(appLocalizations.externalController),
subtitle: Text(appLocalizations.externalControllerDesc),
delegate: SwitchDelegate(
value: hasExternalController,
onChanged: (bool value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
externalController: value
? ExternalControllerStatus.open
: ExternalControllerStatus.close,
),
);
},
),
);
}
}
final generalItems = const [
final generalItems = <Widget>[
LogLevelItem(),
UaItem(),
KeepAliveIntervalItem(),
if (system.isDesktop) KeepAliveIntervalItem(),
TestUrlItem(),
MixedPortItem(),
HostsItem(),

View File

@@ -3,200 +3,185 @@ import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class VPNItem extends StatelessWidget {
class VPNItem extends ConsumerWidget {
const VPNItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: const Text("VPN"),
subtitle: Text(appLocalizations.vpnEnableDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) async {
final config = globalState.appController.config;
config.vpnProps = config.vpnProps.copyWith(
enable: value,
Widget build(BuildContext context, ref) {
final enable =
ref.watch(vpnSettingProvider.select((state) => state.enable));
return ListItem.switchItem(
title: const Text("VPN"),
subtitle: Text(appLocalizations.vpnEnableDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) async {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
enable: value,
),
);
},
),
);
},
},
),
);
}
}
class TUNItem extends StatelessWidget {
class TUNItem extends ConsumerWidget {
const TUNItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) async {
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
enable: value,
Widget build(BuildContext context, ref) {
final enable =
ref.watch(patchClashConfigProvider.select((state) => state.tun.enable));
return ListItem.switchItem(
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) async {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(
enable: value,
),
);
},
),
);
},
},
),
);
}
}
class AllowBypassItem extends StatelessWidget {
class AllowBypassItem extends ConsumerWidget {
const AllowBypassItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
allowBypass: value,
Widget build(BuildContext context, ref) {
final allowBypass =
ref.watch(vpnSettingProvider.select((state) => state.allowBypass));
return ListItem.switchItem(
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
allowBypass: value,
),
);
},
),
);
},
},
),
);
}
}
class VpnSystemProxyItem extends StatelessWidget {
class VpnSystemProxyItem extends ConsumerWidget {
const VpnSystemProxyItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
systemProxy: value,
Widget build(BuildContext context, ref) {
final systemProxy =
ref.watch(vpnSettingProvider.select((state) => state.systemProxy));
return ListItem.switchItem(
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
systemProxy: value,
),
);
},
),
);
},
},
),
);
}
}
class SystemProxyItem extends StatelessWidget {
class SystemProxyItem extends ConsumerWidget {
const SystemProxyItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.networkProps.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
final networkProps = config.networkProps;
config.networkProps = networkProps.copyWith(
systemProxy: value,
Widget build(BuildContext context, ref) {
final systemProxy =
ref.watch(networkSettingProvider.select((state) => state.systemProxy));
return ListItem.switchItem(
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
systemProxy: value,
),
);
},
),
);
},
},
),
);
}
}
class Ipv6Item extends StatelessWidget {
class Ipv6Item extends ConsumerWidget {
const Ipv6Item({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6InboundDesc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
ipv6: value,
Widget build(BuildContext context, ref) {
final ipv6 = ref.watch(vpnSettingProvider.select((state) => state.ipv6));
return ListItem.switchItem(
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6InboundDesc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
ipv6: value,
),
);
},
),
);
},
},
),
);
}
}
class TunStackItem extends StatelessWidget {
class TunStackItem extends ConsumerWidget {
const TunStackItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, TunStack>(
selector: (_, clashConfig) => clashConfig.tun.stack,
builder: (_, stack, __) {
return ListItem.options(
title: Text(appLocalizations.stackMode),
subtitle: Text(stack.name),
delegate: OptionsDelegate<TunStack>(
value: stack,
options: TunStack.values,
textBuilder: (value) => value.name,
onChanged: (value) {
if (value == null) {
return;
}
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
stack: value,
Widget build(BuildContext context, ref) {
final stack =
ref.watch(patchClashConfigProvider.select((state) => state.tun.stack));
return ListItem.options(
title: Text(appLocalizations.stackMode),
subtitle: Text(stack.name),
delegate: OptionsDelegate<TunStack>(
value: stack,
options: TunStack.values,
textBuilder: (value) => value.name,
onChanged: (value) {
if (value == null) {
return;
}
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(
stack: value,
),
);
},
title: appLocalizations.stackMode,
),
);
},
},
title: appLocalizations.stackMode,
),
);
}
}
@@ -204,7 +189,7 @@ class TunStackItem extends StatelessWidget {
class BypassDomainItem extends StatelessWidget {
const BypassDomainItem({super.key});
_initActions(BuildContext context) {
_initActions(BuildContext context, WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context.commonScaffoldState?.actions = [
IconButton(
@@ -218,10 +203,11 @@ class BypassDomainItem extends StatelessWidget {
if (res != true) {
return;
}
final config = globalState.appController.config;
config.networkProps = config.networkProps.copyWith(
bypassDomain: defaultBypassDomain,
);
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
bypassDomain: defaultBypassDomain,
),
);
},
tooltip: appLocalizations.reset,
icon: const Icon(
@@ -238,103 +224,98 @@ class BypassDomainItem extends StatelessWidget {
title: Text(appLocalizations.bypassDomain),
subtitle: Text(appLocalizations.bypassDomainDesc),
delegate: OpenDelegate(
isBlur: false,
isScaffold: true,
blur: false,
title: appLocalizations.bypassDomain,
widget: Selector<Config, List<String>>(
selector: (_, config) => config.networkProps.bypassDomain,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (context, bypassDomain, __) {
_initActions(context);
return ListPage(
widget: Consumer(
builder: (_, ref, __) {
_initActions(context, ref);
final bypassDomain = ref.watch(
networkSettingProvider.select((state) => state.bypassDomain));
return ListInputPage(
title: appLocalizations.bypassDomain,
items: bypassDomain,
titleBuilder: (item) => Text(item),
onChange: (items) {
final config = globalState.appController.config;
config.networkProps = config.networkProps.copyWith(
bypassDomain: List.from(items),
);
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
bypassDomain: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class RouteModeItem extends StatelessWidget {
class RouteModeItem extends ConsumerWidget {
const RouteModeItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, RouteMode>(
selector: (_, clashConfig) => clashConfig.routeMode,
builder: (_, value, __) {
return ListItem<RouteMode>.options(
title: Text(appLocalizations.routeMode),
subtitle: Text(Intl.message("routeMode_${value.name}")),
delegate: OptionsDelegate<RouteMode>(
title: appLocalizations.routeMode,
options: RouteMode.values,
onChanged: (RouteMode? value) {
if (value == null) {
return;
}
final appController = globalState.appController;
appController.clashConfig.routeMode = value;
},
textBuilder: (routeMode) => Intl.message(
"routeMode_${routeMode.name}",
),
value: value,
),
);
},
Widget build(BuildContext context, ref) {
final routeMode =
ref.watch(networkSettingProvider.select((state) => state.routeMode));
return ListItem<RouteMode>.options(
title: Text(appLocalizations.routeMode),
subtitle: Text(Intl.message("routeMode_${routeMode.name}")),
delegate: OptionsDelegate<RouteMode>(
title: appLocalizations.routeMode,
options: RouteMode.values,
onChanged: (RouteMode? value) {
if (value == null) {
return;
}
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
routeMode: value,
),
);
},
textBuilder: (routeMode) => Intl.message(
"routeMode_${routeMode.name}",
),
value: routeMode,
),
);
}
}
class RouteAddressItem extends StatelessWidget {
class RouteAddressItem extends ConsumerWidget {
const RouteAddressItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.routeMode == RouteMode.config,
builder: (_, value, child) {
if (value) {
return child!;
}
return Container();
},
child: ListItem.open(
title: Text(appLocalizations.routeAddress),
subtitle: Text(appLocalizations.routeAddressDesc),
delegate: OpenDelegate(
isBlur: false,
isScaffold: true,
title: appLocalizations.routeAddress,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.includeRouteAddress,
shouldRebuild: (prev, next) =>
!stringListEquality.equals(prev, next),
builder: (context, routeAddress, __) {
return ListPage(
title: appLocalizations.routeAddress,
items: routeAddress,
titleBuilder: (item) => Text(item),
onChange: (items) {
final clashConfig = globalState.appController.clashConfig;
clashConfig.includeRouteAddress =
Set<String>.from(items).toList();
},
);
},
),
extendPageWidth: 360,
Widget build(BuildContext context, ref) {
final bypassPrivate = ref.watch(networkSettingProvider
.select((state) => state.routeMode == RouteMode.bypassPrivate));
if (bypassPrivate) {
return Container();
}
return ListItem.open(
title: Text(appLocalizations.routeAddress),
subtitle: Text(appLocalizations.routeAddressDesc),
delegate: OpenDelegate(
blur: false,
maxWidth: 360,
title: appLocalizations.routeAddress,
widget: Consumer(
builder: (_, ref, __) {
final routeAddress = ref.watch(patchClashConfigProvider
.select((state) => state.tun.routeAddress));
return ListInputPage(
title: appLocalizations.routeAddress,
items: routeAddress,
titleBuilder: (item) => Text(item),
onChange: (items) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(
routeAddress: List.from(items),
),
);
},
);
},
),
),
);
@@ -347,7 +328,8 @@ final networkItems = [
...generateSection(
title: "VPN",
items: [
const SystemProxyItem(),
const VpnSystemProxyItem(),
const BypassDomainItem(),
const AllowBypassItem(),
const Ipv6Item(),
],
@@ -371,10 +353,10 @@ final networkItems = [
),
];
class NetworkListView extends StatelessWidget {
class NetworkListView extends ConsumerWidget {
const NetworkListView({super.key});
_initActions(BuildContext context) {
_initActions(BuildContext context, WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context.commonScaffoldState?.actions = [
IconButton(
@@ -388,9 +370,14 @@ class NetworkListView extends StatelessWidget {
if (res != true) {
return;
}
final appController = globalState.appController;
appController.config.vpnProps = defaultVpnProps;
appController.clashConfig.tun = defaultTun;
ref.read(vpnSettingProvider.notifier).updateState(
(state) => defaultVpnProps,
);
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
tun: defaultTun,
),
);
},
tooltip: appLocalizations.reset,
icon: const Icon(
@@ -402,8 +389,8 @@ class NetworkListView extends StatelessWidget {
}
@override
Widget build(BuildContext context) {
_initActions(context);
Widget build(BuildContext context, ref) {
_initActions(context, ref);
return generateListView(
networkItems,
);

View File

@@ -4,21 +4,23 @@ import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'item.dart';
class ConnectionsFragment extends StatefulWidget {
class ConnectionsFragment extends ConsumerStatefulWidget {
const ConnectionsFragment({super.key});
@override
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
ConsumerState<ConnectionsFragment> createState() =>
_ConnectionsFragmentState();
}
class _ConnectionsFragmentState extends State<ConnectionsFragment>
with ViewMixin {
class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
with PageMixin {
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
const ConnectionsState(),
);
@@ -71,15 +73,20 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
@override
void initState() {
super.initState();
_updateConnections();
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
initViewState();
ref.listenManual(
isCurrentPageProvider(
PageLabel.connections,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
_updateConnections();
}
_handleBlockConnection(String id) async {
@@ -100,56 +107,44 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'connections' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: ValueListenableBuilder<ConnectionsState>(
valueListenable: _connectionsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullConnectionsDesc,
);
}
return CommonScrollBar(
controller: _scrollController,
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
return ValueListenableBuilder<ConnectionsState>(
valueListenable: _connectionsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullConnectionsDesc,
);
},
),
}
return CommonScrollBar(
controller: _scrollController,
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
);
},
);
}
}

View File

@@ -4,9 +4,10 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FindProcessBuilder extends StatelessWidget {
final Widget Function(bool value) builder;
@@ -18,11 +19,15 @@ class FindProcessBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
builder: (_, value, __) {
return Consumer(
builder: (_, ref, __) {
final value = ref.watch(
patchClashConfigProvider.select(
(state) =>
state.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
),
);
return builder(value);
},
);

View File

@@ -1,28 +1,26 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'item.dart';
double _preOffset = 0;
class RequestsFragment extends StatefulWidget {
class RequestsFragment extends ConsumerStatefulWidget {
const RequestsFragment({super.key});
@override
State<RequestsFragment> createState() => _RequestsFragmentState();
ConsumerState<RequestsFragment> createState() => _RequestsFragmentState();
}
class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
class _RequestsFragmentState extends ConsumerState<RequestsFragment>
with PageMixin {
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey();
final _requestsStateNotifier =
ValueNotifier<ConnectionsState>(const ConnectionsState());
List<Connection> _requests = [];
@@ -31,8 +29,6 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
);
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0;
@override
@@ -51,26 +47,36 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
@override
void initState() {
super.initState();
final appController = globalState.appController;
final appState = appController.appState;
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: appState.requests,
connections: globalState.appState.requests.list,
);
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
initViewState();
ref.listenManual(
isCurrentPageProvider(
PageLabel.requests,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
ref.listenManual(
requestsProvider.select((state) => state.list),
(prev, next) {
if (!connectionListEquality.equals(prev, next)) {
_requests = next;
updateRequestsThrottler();
}
},
fireImmediately: true,
);
}
double _calcCacheHeight(Connection item) {
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
if (cacheHeight != null) {
return cacheHeight;
}
final size = globalState.measure.computeTextSize(
Text(
item.desc,
@@ -91,14 +97,13 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
final lines = (chainSize.height / baseHeight).round();
final computerHeight =
size.height + chainSize.height + 24 + 24 * (lines - 1);
_cacheDynamicHeightMap.put(item.id, computerHeight);
return computerHeight;
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear();
_key.currentState?.clearCache();
}
}
@@ -107,26 +112,9 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
_requestsStateNotifier.dispose();
_scrollController.dispose();
_currentMaxWidth = 0;
_cacheDynamicHeightMap.clear();
super.dispose();
}
Widget _wrapPage(Widget child) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'requests' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: child,
);
}
updateRequestsThrottler() {
throttler.call("request", () {
final isEquality = connectionListEquality.equals(
@@ -144,93 +132,80 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
}, duration: commonDuration);
}
Widget _wrapRequestsUpdate(Widget child) {
return Selector<AppState, List<Connection>>(
selector: (_, appState) => appState.requests,
shouldRebuild: (prev, next) {
final isEquality = connectionListEquality.equals(prev, next);
if (!isEquality) {
_requests = next;
updateRequestsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
return FindProcessBuilder(builder: (value) {
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return _wrapPage(
_wrapRequestsUpdate(
ValueListenableBuilder<ConnectionsState>(
valueListenable: _requestsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
final items = connections
.map<Widget>(
(connection) => ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
return ValueListenableBuilder<ConnectionsState>(
valueListenable: _requestsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
final items = connections
.map<Widget>(
(connection) => ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index, __) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
),
),
),
);
},
),
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: CacheItemExtentListView(
key: _key,
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
keyBuilder: (int index) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return "divider";
}
final connection = connections[(index / 2).floor()];
return connection.id;
},
),
),
),
);
},
);
});
},

View File

@@ -2,31 +2,42 @@ import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'widgets/start_button.dart';
class DashboardFragment extends StatefulWidget {
class DashboardFragment extends ConsumerStatefulWidget {
const DashboardFragment({super.key});
@override
State<DashboardFragment> createState() => _DashboardFragmentState();
ConsumerState<DashboardFragment> createState() => _DashboardFragmentState();
}
class _DashboardFragmentState extends State<DashboardFragment> {
class _DashboardFragmentState extends ConsumerState<DashboardFragment>
with PageMixin {
final key = GlobalKey<SuperGridState>();
_initScaffold(bool isCurrent) {
if (!isCurrent) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.floatingActionButton = const StartButton();
commonScaffoldState?.actions = [
@override
initState() {
ref.listenManual(
isCurrentPageProvider(PageLabel.dashboard),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
return super.initState();
}
@override
Widget? get floatingActionButton => const StartButton();
@override
List<Widget> get actions => [
ValueListenableBuilder(
valueListenable: key.currentState!.addedChildrenNotifier,
builder: (_, addedChildren, child) {
@@ -67,72 +78,59 @@ class _DashboardFragmentState extends State<DashboardFragment> {
},
),
];
});
_handleSave(List<GridItem> girdItems, WidgetRef ref) {
final dashboardWidgets = girdItems
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
);
}
@override
Widget build(BuildContext context) {
return ActiveBuilder(
label: "dashboard",
builder: (isCurrent, child) {
_initScaffold(isCurrent);
return child!;
},
child: Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: Selector2<AppState, Config, DashboardState>(
selector: (_, appState, config) => DashboardState(
dashboardWidgets: config.appSetting.dashboardWidgets,
viewWidth: appState.viewWidth,
),
builder: (_, state, ___) {
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8);
return SuperGrid(
key: key,
crossAxisCount: columns,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
...state.dashboardWidgets
.where(
(item) => item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map(
(item) => item.widget,
final dashboardState = ref.watch(dashboardStateProvider);
final columns = max(4 * ((dashboardState.viewWidth / 350).ceil()), 8);
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: SuperGrid(
key: key,
crossAxisCount: columns,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
...dashboardState.dashboardWidgets
.where(
(item) => item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map(
(item) => item.widget,
),
],
onSave: (girdItems) {
_handleSave(girdItems, ref);
},
addedItemsBuilder: (girdItems) {
return DashboardWidget.values
.where(
(item) =>
!girdItems.contains(item.widget) &&
item.platforms.contains(
SupportPlatform.currentPlatform,
),
],
onSave: (girdItems) {
final dashboardWidgets = girdItems
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
dashboardWidgets: dashboardWidgets,
);
},
addedItemsBuilder: (girdItems) {
return DashboardWidget.values
.where(
(item) =>
!girdItems.contains(item.widget) &&
item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map((item) => item.widget)
.toList();
},
);
},
),
)
.map((item) => item.widget)
.toList();
},
),
),
);

View File

@@ -1,58 +0,0 @@
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CoreInfo extends StatelessWidget {
const CoreInfo({super.key});
@override
Widget build(BuildContext context) {
return Selector<AppState, VersionInfo?>(
selector: (_, appState) => appState.versionInfo,
builder: (_, versionInfo, __) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.coreInfo,
iconData: Icons.memory,
),
child: Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
flex: 1,
child: Text(
versionInfo?.clashName ?? '',
style: context
.textTheme
.titleMedium
?.toSoftBold,
),
),
const SizedBox(
height: 8,
),
Flexible(
flex: 1,
child: Text(
versionInfo?.version ?? '',
style: context
.textTheme
.titleLarge
?.toSoftBold,
),
),
],
),
),
);
},
);
}
}

View File

@@ -1,9 +1,9 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class IntranetIP extends StatelessWidget {
const IntranetIP({super.key});
@@ -28,15 +28,15 @@ class IntranetIP extends StatelessWidget {
children: [
SizedBox(
height: globalState.measure.bodyMediumHeight + 2,
child: Selector<AppFlowingState, String?>(
selector: (_, appFlowingState) => appFlowingState.localIp,
builder: (_, value, __) {
return FadeBox(
child: value != null
child: Consumer(
builder: (_, ref, __) {
final localIp = ref.watch(localIpProvider);
return FadeThroughBox(
child: localIp != null
? TooltipText(
text: Text(
value.isNotEmpty
? value
localIp.isNotEmpty
? localIp
: appLocalizations.noNetwork,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
@@ -48,7 +48,9 @@ class IntranetIP extends StatelessWidget {
padding: EdgeInsets.all(2),
child: AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);

View File

@@ -39,7 +39,7 @@ class _MemoryInfoState extends State<MemoryInfo> {
_memoryInfoStateNotifier.value = TrafficValue(
value: clashLib != null ? rss : await clashCore.getMemory() + rss,
);
timer = Timer(Duration(seconds: 5), () async {
timer = Timer(Duration(seconds: 2), () async {
_updateMemory();
});
});
@@ -48,7 +48,7 @@ class _MemoryInfoState extends State<MemoryInfo> {
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(2),
height: getWidgetHeight(1),
child: CommonCard(
info: Info(
iconData: Icons.memory,
@@ -57,12 +57,12 @@ class _MemoryInfoState extends State<MemoryInfo> {
onPressed: () {
clashCore.requestGc();
},
child: ValueListenableBuilder(
valueListenable: _memoryInfoStateNotifier,
builder: (_, trafficValue, __) {
return Column(
children: [
Padding(
child: Column(
children: [
ValueListenableBuilder(
valueListenable: _memoryInfoStateNotifier,
builder: (_, trafficValue, __) {
return Padding(
padding: baseInfoEdgeInsets.copyWith(
bottom: 0,
top: 12,
@@ -71,46 +71,94 @@ class _MemoryInfoState extends State<MemoryInfo> {
children: [
Text(
trafficValue.showValue,
style: context.textTheme.titleLarge?.toLight,
style:
context.textTheme.bodyMedium?.toLight.adjustSize(1),
),
SizedBox(
width: 8,
),
Text(
trafficValue.showUnit,
style: context.textTheme.titleLarge?.toLight,
style:
context.textTheme.bodyMedium?.toLight.adjustSize(1),
)
],
),
),
Flexible(
child: Stack(
children: [
Positioned.fill(
child: WaveView(
waveAmplitude: 12.0,
waveFrequency: 0.35,
waveColor: context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1)
.toLighter,
),
),
Positioned.fill(
child: WaveView(
waveAmplitude: 12.0,
waveFrequency: 0.9,
waveColor: context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1),
),
),
],
),
)
],
);
},
);
},
),
],
),
),
);
}
}
// class AnimatedCounter extends StatefulWidget {
// final double value;
// final TextStyle? style;
//
// const AnimatedCounter({
// super.key,
// required this.value,
// this.style,
// });
//
// @override
// State<AnimatedCounter> createState() => _AnimatedCounterState();
// }
//
// class _AnimatedCounterState extends State<AnimatedCounter> {
// late double _previousValue;
// late double _currentValue;
//
// @override
// void initState() {
// super.initState();
// _previousValue = widget.value;
// _currentValue = widget.value;
// }
//
// @override
// void didUpdateWidget(AnimatedCounter oldWidget) {
// super.didUpdateWidget(oldWidget);
// if (oldWidget.value != widget.value) {
// // if (_previousValue == _currentValue) {
// // _previousValue = widget.value;
// // _currentValue = widget.value;
// // return;
// // }
// _currentValue = widget.value;
// }
// }
//
// @override
// void dispose() {
// super.dispose();
// }
//
// @override
// Widget build(BuildContext context) {
// return Text(
// _currentValue.fixed(decimals: 1),
// style: widget.style,
// );
// return TweenAnimationBuilder(
// tween: Tween(
// begin: _previousValue,
// end: _currentValue,
// ),
// onEnd: () {
// _previousValue = _currentValue;
// },
// duration: Duration(seconds: 6),
// curve: Curves.easeOut,
// builder: (_, value, ___) {
// return Text(
// value.fixed(decimals: 1),
// style: widget.style,
// );
// },
// );
// }
// }

View File

@@ -4,38 +4,47 @@ import 'package:dio/dio.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
isTesting: false,
isLoading: true,
ipInfo: null,
),
);
class NetworkDetection extends StatefulWidget {
class NetworkDetection extends ConsumerStatefulWidget {
const NetworkDetection({super.key});
@override
State<NetworkDetection> createState() => _NetworkDetectionState();
ConsumerState<NetworkDetection> createState() => _NetworkDetectionState();
}
class _NetworkDetectionState extends State<NetworkDetection> {
class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
bool? _preIsStart;
Timer? _setTimeoutTimer;
CancelToken? cancelToken;
Completer? checkedCompleter;
@override
void initState() {
ref.listenManual(checkIpNumProvider, (prev, next) {
if (prev != next) {
_startCheck();
}
});
if (!_networkDetectionState.value.isTesting &&
_networkDetectionState.value.isLoading) {
_startCheck();
}
super.initState();
}
_startCheck() async {
await checkedCompleter?.future;
if (cancelToken != null) {
cancelToken!.cancel();
cancelToken = null;
@@ -47,15 +56,18 @@ class _NetworkDetectionState extends State<NetworkDetection> {
}
_checkIp() async {
final appState = globalState.appController.appState;
final appFlowingState = globalState.appController.appFlowingState;
final appState = globalState.appState;
final isInit = appState.isInit;
if (!isInit) return;
final isStart = appFlowingState.isStart;
if (_preIsStart == false && _preIsStart == isStart) return;
final isStart = appState.runTime != null;
if (_preIsStart == false &&
_preIsStart == isStart &&
_networkDetectionState.value.ipInfo != null) {
return;
}
_clearSetTimeoutTimer();
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true,
isLoading: true,
ipInfo: null,
);
_preIsStart = isStart;
@@ -65,16 +77,16 @@ class _NetworkDetectionState extends State<NetworkDetection> {
}
cancelToken = CancelToken();
try {
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true,
);
final ipInfo = await request.checkIp(cancelToken: cancelToken);
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: false,
);
if (ipInfo != null) {
checkedCompleter = Completer();
checkedCompleter?.complete(
Future.delayed(
Duration(milliseconds: 3000),
),
);
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: false,
isLoading: false,
ipInfo: ipInfo,
);
return;
@@ -82,14 +94,14 @@ class _NetworkDetectionState extends State<NetworkDetection> {
_clearSetTimeoutTimer();
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: false,
isLoading: false,
ipInfo: null,
);
});
} catch (e) {
if (e.toString() == "cancelled") {
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true,
isLoading: true,
ipInfo: null,
);
}
@@ -109,24 +121,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
}
}
_checkIpContainer(Widget child) {
return Selector<AppState, num>(
selector: (_, appState) {
return appState.checkIpNum;
},
shouldRebuild: (prev, next) {
if (prev != next) {
_startCheck();
}
return prev != next;
},
builder: (_, checkIpNum, child) {
return child!;
},
child: child,
);
}
_countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase();
if (code.length != 2) {
@@ -141,109 +135,130 @@ class _NetworkDetectionState extends State<NetworkDetection> {
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: _checkIpContainer(
ValueListenableBuilder<NetworkDetectionState>(
valueListenable: _networkDetectionState,
builder: (_, state, __) {
final ipInfo = state.ipInfo;
final isTesting = state.isTesting;
return CommonCard(
onPressed: () {},
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: globalState.measure.titleMediumHeight + 16,
padding: baseInfoEdgeInsets.copyWith(
bottom: 0,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
ipInfo != null
? Text(
_countryCodeToEmoji(
ipInfo.countryCode,
),
style: Theme.of(context)
.textTheme
.titleMedium
?.toLight
.copyWith(
fontFamily: FontFamily.twEmoji.value,
),
)
: Icon(
Icons.network_check,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
child: ValueListenableBuilder<NetworkDetectionState>(
valueListenable: _networkDetectionState,
builder: (_, state, __) {
final ipInfo = state.ipInfo;
final isLoading = state.isLoading;
return CommonCard(
onPressed: () {},
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: globalState.measure.titleMediumHeight + 16,
padding: baseInfoEdgeInsets.copyWith(
bottom: 0,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
ipInfo != null
? Text(
_countryCodeToEmoji(
ipInfo.countryCode,
),
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.networkDetection,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(
color: context.colorScheme.onSurfaceVariant,
.titleMedium
?.toLight
.copyWith(
fontFamily: FontFamily.twEmoji.value,
),
)
: Icon(
Icons.network_check,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.networkDetection,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
),
],
),
),
SizedBox(width: 2),
AspectRatio(
aspectRatio: 1,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
text: appLocalizations.detectionTip,
),
cancelable: false,
);
},
icon: Icon(
size: 16,
Icons.info_outline,
color: context.colorScheme.onSurfaceVariant,
),
),
)
],
),
Container(
padding: baseInfoEdgeInsets.copyWith(
top: 0,
),
child: SizedBox(
height: globalState.measure.bodyMediumHeight + 2,
child: FadeBox(
child: ipInfo != null
? TooltipText(
text: Text(
ipInfo.ip,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
: FadeBox(
child: isTesting == false && ipInfo == null
? Text(
"timeout",
style: context.textTheme.bodyMedium
?.copyWith(color: Colors.red)
.adjustSize(1),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Container(
padding: const EdgeInsets.all(2),
child: const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
Container(
padding: baseInfoEdgeInsets.copyWith(
top: 0,
),
child: SizedBox(
height: globalState.measure.bodyMediumHeight + 2,
child: FadeThroughBox(
child: ipInfo != null
? TooltipText(
text: Text(
ipInfo.ip,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
: FadeThroughBox(
child: isLoading == false && ipInfo == null
? Text(
"timeout",
style: context.textTheme.bodyMedium
?.copyWith(color: Colors.red)
.adjustSize(1),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Container(
padding: const EdgeInsets.all(2),
child: const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
),
),
),
),
)
],
),
);
},
),
),
)
],
),
);
},
),
);
}

View File

@@ -1,8 +1,9 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class NetworkSpeed extends StatefulWidget {
const NetworkSpeed({super.key});
@@ -40,7 +41,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
@override
Widget build(BuildContext context) {
final color = context.colorScheme.onSurfaceVariant.toLight;
final color = context.colorScheme.onSurfaceVariant.opacity80;
return SizedBox(
height: getWidgetHeight(2),
child: CommonCard(
@@ -49,9 +50,9 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
label: appLocalizations.networkSpeed,
iconData: Icons.speed_sharp,
),
child: Selector<AppFlowingState, List<Traffic>>(
selector: (_, appFlowingState) => appFlowingState.traffics,
builder: (_, traffics, __) {
child: Consumer(
builder: (_, ref, __) {
final traffics = ref.watch(trafficsProvider).list;
return Stack(
children: [
Positioned.fill(

View File

@@ -1,11 +1,11 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class OutboundMode extends StatelessWidget {
const OutboundMode({super.key});
@@ -15,9 +15,10 @@ class OutboundMode extends StatelessWidget {
final height = getWidgetHeight(2);
return SizedBox(
height: height,
child: Selector<ClashConfig, Mode>(
selector: (_, clashConfig) => clashConfig.mode,
builder: (_, mode, __) {
child: Consumer(
builder: (_, ref, __) {
final mode =
ref.watch(patchClashConfigProvider.select((state) => state.mode));
return CommonCard(
onPressed: () {},
info: Info(
@@ -37,7 +38,7 @@ class OutboundMode extends StatelessWidget {
for (final item in Mode.values)
Flexible(
child: ListItem.radio(
prue: true,
dense: true,
horizontalTitleGap: 4,
padding: const EdgeInsets.only(
left: 12,

View File

@@ -1,78 +1,83 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/config/network.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TUNButton extends StatelessWidget {
const TUNButton({super.key});
@override
Widget build(BuildContext context) {
return LocaleBuilder(
builder: (_) => SizedBox(
height: getWidgetHeight(1),
child: CommonCard(
onPressed: () {
showSheet(
context: context,
body: generateListView(generateSection(
items: [
if (system.isDesktop) const TUNItem(),
const TunStackItem(),
],
)),
title: appLocalizations.tun,
);
},
info: Info(
label: appLocalizations.tun,
iconData: Icons.stacked_line_chart,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
return SizedBox(
height: getWidgetHeight(1),
child: CommonCard(
onPressed: () {
showSheet(
context: context,
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: generateListView(
generateSection(
items: [
if (system.isDesktop) const TUNItem(),
const TunStackItem(),
],
),
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, enable, __) {
return Switch(
value: enable,
onChanged: (value) {
final clashConfig =
globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
enable: value,
);
},
);
},
)
],
),
title: appLocalizations.tun,
);
},
);
},
info: Info(
label: appLocalizations.tun,
iconData: Icons.stacked_line_chart,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
),
),
Consumer(
builder: (_, ref, __) {
final enable = ref.watch(patchClashConfigProvider
.select((state) => state.tun.enable));
return Switch(
value: enable,
onChanged: (value) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(
enable: value,
),
);
},
);
},
)
],
),
),
),
@@ -87,67 +92,73 @@ class SystemProxyButton extends StatelessWidget {
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: LocaleBuilder(
builder: (_) => CommonCard(
onPressed: () {
showSheet(
context: context,
body: generateListView(
generateSection(
items: [
SystemProxyItem(),
BypassDomainItem(),
],
),
),
title: appLocalizations.systemProxy,
);
},
info: Info(
label: appLocalizations.systemProxy,
iconData: Icons.shuffle,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
child: CommonCard(
onPressed: () {
showSheet(
context: context,
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: generateListView(
generateSection(
items: [
SystemProxyItem(),
BypassDomainItem(),
],
),
),
Selector<Config, bool>(
selector: (_, config) => config.networkProps.systemProxy,
builder: (_, systemProxy, __) {
return Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: systemProxy,
onChanged: (value) {
final config = globalState.appController.config;
config.networkProps =
config.networkProps.copyWith(systemProxy: value);
},
);
},
)
],
),
title: appLocalizations.systemProxy,
);
},
);
},
info: Info(
label: appLocalizations.systemProxy,
iconData: Icons.shuffle,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
),
),
Consumer(
builder: (_, ref, __) {
final systemProxy = ref.watch(networkSettingProvider
.select((state) => state.systemProxy));
return Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: systemProxy,
onChanged: (value) {
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
systemProxy: value,
),
);
},
);
},
)
],
),
),
),

View File

@@ -1,8 +1,9 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class StartButton extends StatefulWidget {
const StartButton({super.key});
@@ -19,7 +20,7 @@ class _StartButtonState extends State<StartButton>
@override
void initState() {
super.initState();
isStart = globalState.appController.appFlowingState.isStart;
isStart = globalState.appState.runTime != null;
_controller = AnimationController(
vsync: this,
value: isStart ? 1 : 0,
@@ -34,11 +35,10 @@ class _StartButtonState extends State<StartButton>
}
handleSwitchStart() {
final appController = globalState.appController;
if (isStart == appController.appFlowingState.isStart) {
if (isStart == globalState.appState.isStart) {
isStart = !isStart;
updateController();
appController.updateStatus(isStart);
globalState.appController.updateStatus(isStart);
}
}
@@ -50,31 +50,24 @@ class _StartButtonState extends State<StartButton>
}
}
Widget _updateControllerContainer(Widget child) {
return Selector<AppFlowingState, bool>(
selector: (_, appFlowingState) => appFlowingState.isStart,
builder: (_, isStart, child) {
if (isStart != this.isStart) {
this.isStart = isStart;
updateController();
}
return child!;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, StartButtonSelectorState>(
selector: (_, appState, config) => StartButtonSelectorState(
isInit: appState.isInit,
hasProfile: config.profiles.isNotEmpty,
),
builder: (_, state, child) {
return Consumer(
builder: (_, ref, child) {
final state = ref.watch(startButtonSelectorStateProvider);
if (!state.isInit || !state.hasProfile) {
return Container();
}
ref.listenManual(
runTimeProvider.select((state) => state != null),
(prev, next) {
if (next != isStart) {
isStart = next;
updateController();
}
},
fireImmediately: true,
);
final textWidth = globalState.measure
.computeTextSize(
Text(
@@ -86,53 +79,51 @@ class _StartButtonState extends State<StartButton>
)
.width +
16;
return _updateControllerContainer(
AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56 + textWidth * _controller.value,
height: 56,
child: FloatingActionButton(
heroTag: null,
onPressed: () {
handleSwitchStart();
},
child: Row(
children: [
Container(
width: 56,
height: 56,
alignment: Alignment.center,
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _controller,
),
return AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56 + textWidth * _controller.value,
height: 56,
child: FloatingActionButton(
heroTag: null,
onPressed: () {
handleSwitchStart();
},
child: Row(
children: [
Container(
width: 56,
height: 56,
alignment: Alignment.center,
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _controller,
),
Expanded(
child: ClipRect(
child: OverflowBox(
maxWidth: textWidth,
child: Container(
alignment: Alignment.centerLeft,
child: child!,
),
),
Expanded(
child: ClipRect(
child: OverflowBox(
maxWidth: textWidth,
child: Container(
alignment: Alignment.centerLeft,
child: child!,
),
),
),
],
),
),
],
),
);
},
child: child,
),
),
);
},
child: child,
);
},
child: Selector<AppFlowingState, int?>(
selector: (_, appFlowingState) => appFlowingState.runTime,
builder: (_, int? value, __) {
final text = other.getTimeText(value);
child: Consumer(
builder: (_, ref, __) {
final runTime = ref.watch(runTimeProvider);
final text = other.getTimeText(runTime);
return Text(
text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,

View File

@@ -2,15 +2,16 @@ import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TrafficUsage extends StatelessWidget {
const TrafficUsage({super.key});
Widget getTrafficDataItem(
Widget _buildTrafficDataItem(
BuildContext context,
Icon icon,
TrafficValue trafficValue,
@@ -50,10 +51,8 @@ class TrafficUsage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final primaryColor =
context.colorScheme.surfaceContainer.blendDarken(context, factor: 0.2);
final secondaryColor =
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3);
final primaryColor = globalState.theme.darken3PrimaryContainer;
final secondaryColor = globalState.theme.darken2SecondaryContainer;
return SizedBox(
height: getWidgetHeight(2),
child: CommonCard(
@@ -62,9 +61,9 @@ class TrafficUsage extends StatelessWidget {
iconData: Icons.data_saver_off,
),
onPressed: () {},
child: Selector<AppFlowingState, Traffic>(
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
builder: (_, totalTraffic, __) {
child: Consumer(
builder: (_, ref, __) {
final totalTraffic = ref.watch(totalTrafficProvider);
final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down;
return Padding(
@@ -188,7 +187,7 @@ class TrafficUsage extends StatelessWidget {
),
),
),
getTrafficDataItem(
_buildTrafficDataItem(
context,
Icon(
Icons.arrow_upward,
@@ -200,7 +199,7 @@ class TrafficUsage extends StatelessWidget {
const SizedBox(
height: 8,
),
getTrafficDataItem(
_buildTrafficDataItem(
context,
Icon(
Icons.arrow_downward,

View File

@@ -1,13 +1,15 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/card.dart';
import 'package:fl_clash/widgets/dialog.dart';
import 'package:fl_clash/widgets/list.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
extension IntlExt on Intl {
static actionMessage(String messageText) =>
@@ -38,29 +40,20 @@ class HotKeyFragment extends StatelessWidget {
itemCount: HotAction.values.length,
itemBuilder: (_, index) {
final hotAction = HotAction.values[index];
return Selector<Config, HotKeyAction>(
selector: (_, config) {
final index = config.hotKeyActions.indexWhere(
(item) => item.action == hotAction,
);
return index != -1
? config.hotKeyActions[index]
: HotKeyAction(
action: hotAction,
);
},
builder: (_, value, __) {
return Consumer(
builder: (_, ref, __) {
final hotKeyAction = ref.watch(getHotKeyActionProvider(hotAction));
return ListItem(
title: Text(IntlExt.actionMessage(hotAction.name)),
subtitle: Text(
getSubtitle(value),
getSubtitle(hotKeyAction),
style: context.textTheme.bodyMedium
?.copyWith(color: context.colorScheme.primary),
),
onTap: () {
globalState.showCommonDialog(
child: HotKeyRecorder(
hotKeyAction: value,
hotKeyAction: hotKeyAction,
),
);
},
@@ -121,8 +114,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
_handleRemove() {
Navigator.of(context).pop();
final config = globalState.appController.config;
config.updateOrAddHotKeyAction(
globalState.appController.updateOrAddHotKeyAction(
hotKeyActionNotifier.value.copyWith(
modifiers: {},
key: null,
@@ -132,7 +124,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
_handleConfirm() {
Navigator.of(context).pop();
final config = globalState.appController.config;
final config = globalState.config;
final currentHotkeyAction = hotKeyActionNotifier.value;
if (currentHotkeyAction.key == null ||
currentHotkeyAction.modifiers.isEmpty) {
@@ -158,16 +150,35 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
);
return;
}
config.updateOrAddHotKeyAction(
globalState.appController.updateOrAddHotKeyAction(
currentHotkeyAction,
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(IntlExt.actionMessage((widget.hotKeyAction.action.name))),
content: ValueListenableBuilder(
return CommonDialog(
title: IntlExt.actionMessage(widget.hotKeyAction.action.name),
actions: [
TextButton(
onPressed: () {
_handleRemove();
},
child: Text(appLocalizations.remove),
),
const SizedBox(
width: 8,
),
TextButton(
onPressed: () {
_handleConfirm();
},
child: Text(
appLocalizations.confirm,
),
),
],
child: ValueListenableBuilder(
valueListenable: hotKeyActionNotifier,
builder: (_, hotKeyAction, ___) {
final key = hotKeyAction.key;
@@ -200,25 +211,6 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
);
},
),
actions: [
TextButton(
onPressed: () {
_handleRemove();
},
child: Text(appLocalizations.remove),
),
const SizedBox(
width: 8,
),
TextButton(
onPressed: () {
_handleConfirm();
},
child: Text(
appLocalizations.confirm,
),
),
],
);
}
}

View File

@@ -1,40 +1,63 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/models.dart';
import '../widgets/widgets.dart';
double _preOffset = 0;
class LogsFragment extends StatefulWidget {
class LogsFragment extends ConsumerStatefulWidget {
const LogsFragment({super.key});
@override
State<LogsFragment> createState() => _LogsFragmentState();
ConsumerState<LogsFragment> createState() => _LogsFragmentState();
}
class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
final _scrollController = ScrollController(
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
);
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0;
final GlobalKey<CacheItemExtentListViewState> _key = GlobalKey();
List<Log> _logs = [];
@override
void initState() {
super.initState();
final appController = globalState.appController;
final appFlowingState = appController.appFlowingState;
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: appFlowingState.logs,
logs: globalState.appState.logs.list,
);
ref.listenManual(
logsProvider.select((state) => state.list),
(prev, next) {
if (prev != next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
}
},
fireImmediately: true,
);
ref.listenManual(
isCurrentPageProvider(
PageLabel.logs,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
}
@@ -67,14 +90,13 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
void dispose() {
_logsStateNotifier.dispose();
_scrollController.dispose();
_cacheDynamicHeightMap.clear();
super.dispose();
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear();
_key.currentState?.clearCache();
}
}
@@ -93,33 +115,19 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
);
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
super.initViewState();
});
}
double _calcCacheHeight(String text) {
final cacheHeight = _cacheDynamicHeightMap.get(text);
if (cacheHeight != null) {
return cacheHeight;
}
final size = globalState.measure.computeTextSize(
Text(
text,
style: globalState.appController.context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
);
_cacheDynamicHeightMap.put(text, size.height);
return size.height;
}
double _getItemHeight(Log log) {
final measure = globalState.measure;
final bodySmallHeight = measure.bodySmallHeight;
final bodyMediumHeight = measure.bodyMediumHeight;
final height = _calcCacheHeight(log.payload ?? "");
final height = globalState.measure
.computeTextSize(
Text(
log.payload ?? "",
style: globalState.appController.context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
)
.height;
return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
}
@@ -140,98 +148,75 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
}, duration: commonDuration);
}
Widget _wrapLogsUpdate(Widget child) {
return Selector<AppFlowingState, List<Log>>(
selector: (_, appFlowingState) => appFlowingState.logs,
shouldRebuild: (prev, next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
_handleTryClearCache(constraints.maxWidth - 40);
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'logs' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: _wrapLogsUpdate(
Align(
alignment: Alignment.topCenter,
child: ValueListenableBuilder<LogsState>(
valueListenable: _logsStateNotifier,
builder: (_, state, __) {
final logs = state.list;
if (logs.isEmpty) {
return NullStatus(
label: appLocalizations.nullLogsDesc,
);
}
final items = logs
.map<Widget>(
(log) => LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemBuilder: (_, index) {
return items[index];
},
itemExtentBuilder: (index, __) {
final item = items[index];
if (item.runtimeType == Divider) {
return 0;
}
final log = logs[(index / 2).floor()];
return _getItemHeight(log);
},
itemCount: items.length,
),
return Align(
alignment: Alignment.topCenter,
child: ValueListenableBuilder<LogsState>(
valueListenable: _logsStateNotifier,
builder: (_, state, __) {
final logs = state.list;
if (logs.isEmpty) {
return NullStatus(
label: appLocalizations.nullLogsDesc,
);
}
final items = logs
.map<Widget>(
(log) => LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
);
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
),
),
child: CommonScrollBar(
controller: _scrollController,
child: CacheItemExtentListView(
key: _key,
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemBuilder: (_, index) {
return items[index];
},
itemExtentBuilder: (index) {
final item = items[index];
if (item.runtimeType == Divider) {
return 0;
}
final log = logs[(index / 2).floor()];
return _getItemHeight(log);
},
itemCount: items.length,
keyBuilder: (int index) {
final item = items[index];
if (item.runtimeType == Divider) {
return "divider";
}
final log = logs[(index / 2).floor()];
return log.payload ?? "";
},
),
),
);
},
),
);
},
@@ -287,11 +272,3 @@ class LogItem extends StatelessWidget {
);
}
}
class NoGlowScrollBehavior extends ScrollBehavior {
@override
Widget buildOverscrollIndicator(
BuildContext context, Widget child, ScrollableDetails details) {
return child; // 禁用过度滚动效果
}
}

View File

@@ -50,19 +50,19 @@ class AddProfile extends StatelessWidget {
return ListView(
children: [
ListItem(
leading: const Icon(Icons.qr_code),
leading: const Icon(Icons.qr_code_sharp),
title: Text(appLocalizations.qrcode),
subtitle: Text(appLocalizations.qrcodeDesc),
onTap: _toScan,
),
ListItem(
leading: const Icon(Icons.upload_file),
leading: const Icon(Icons.upload_file_sharp),
title: Text(appLocalizations.file),
subtitle: Text(appLocalizations.fileDesc),
onTap: _handleAddProfileFormFile,
),
ListItem(
leading: const Icon(Icons.cloud_download),
leading: const Icon(Icons.cloud_download_sharp),
title: Text(appLocalizations.url),
subtitle: Text(appLocalizations.urlDesc),
onTap: _toAdd,
@@ -90,9 +90,15 @@ class _URLFormDialogState extends State<URLFormDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.importFromURL),
content: SizedBox(
return CommonDialog(
title: appLocalizations.importFromURL,
actions: [
TextButton(
onPressed: _handleAddProfileFormURL,
child: Text(appLocalizations.submit),
)
],
child: SizedBox(
width: 300,
child: Wrap(
runSpacing: 16,
@@ -109,12 +115,6 @@ class _URLFormDialogState extends State<URLFormDialog> {
],
),
),
actions: [
TextButton(
onPressed: _handleAddProfileFormURL,
child: Text(appLocalizations.submit),
)
],
);
}
}

View File

@@ -80,9 +80,9 @@ class _EditProfileState extends State<EditProfile> {
);
}
}
appController.setProfile(await profile.saveFile(fileData!));
appController.setProfileAndAutoApply(await profile.saveFile(fileData!));
} else if (!hasUpdate) {
appController.setProfile(profile);
appController.setProfileAndAutoApply(profile);
} else {
globalState.homeScaffoldKey.currentState?.loadingRun(
() async {
@@ -282,7 +282,7 @@ class _EditProfileState extends State<EditProfile> {
ValueListenableBuilder<FileInfo?>(
valueListenable: fileInfoNotifier,
builder: (_, fileInfo, __) {
return FadeBox(
return FadeThroughBox(
child: fileInfo == null
? Container()
: ListItem(
@@ -324,15 +324,13 @@ class _EditProfileState extends State<EditProfile> {
},
),
];
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, __) {
if (didPop) return;
return CommonPopScope(
onPop: () {
if (fileData == null) {
Navigator.of(context).pop();
return;
return true;
}
_handleBack();
return false;
},
child: FloatLayout(
floatingWidget: FloatWrapper(

View File

@@ -0,0 +1,957 @@
import 'dart:ui';
import 'package:fl_clash/clash/core.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class OverrideProfile extends StatefulWidget {
final String profileId;
const OverrideProfile({
super.key,
required this.profileId,
});
@override
State<OverrideProfile> createState() => _OverrideProfileState();
}
class _OverrideProfileState extends State<OverrideProfile> {
final GlobalKey<CacheItemExtentListViewState> _ruleListKey = GlobalKey();
final _controller = ScrollController();
double _currentMaxWidth = 0;
_initState(WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(milliseconds: 300), () async {
final snippet = await clashCore.getProfile(widget.profileId);
final overrideData = ref.read(
getProfileOverrideDataProvider(widget.profileId),
);
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith(
snippet: snippet,
overrideData: overrideData,
),
);
});
});
}
_handleSave(WidgetRef ref, OverrideData overrideData) {
ref.read(profilesProvider.notifier).updateProfile(
widget.profileId,
(state) => state.copyWith(
overrideData: overrideData,
),
);
}
_handleDelete(WidgetRef ref) async {
final res = await globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.deleteRuleTip),
);
if (res != true) {
return;
}
final selectedRules = ref.read(
profileOverrideStateProvider.select(
(state) => state.selectedRules,
),
);
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) {
final overrideRule = state.overrideData!.rule.updateRules(
(rules) => List.from(
rules.where(
(item) => !selectedRules.contains(item.id),
),
),
);
return state.copyWith.overrideData!(
rule: overrideRule,
);
},
);
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith(isEdit: false, selectedRules: {}),
);
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_ruleListKey.currentState?.clearCache();
}
}
_buildContent() {
return Consumer(
builder: (_, ref, child) {
final isInit = ref.watch(
profileOverrideStateProvider.select(
(state) => state.snippet != null && state.overrideData != null,
),
);
if (!isInit) {
return Center(
child: CircularProgressIndicator(),
);
}
return FadeBox(
child: !isInit
? Center(
child: CircularProgressIndicator(),
)
: child!,
);
},
child: LayoutBuilder(
builder: (_, constraints) {
_handleTryClearCache(constraints.maxWidth - 104);
return CommonAutoHiddenScrollBar(
controller: _controller,
child: CustomScrollView(
controller: _controller,
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: 16,
),
),
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: OverrideSwitch(),
),
),
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.only(
left: 8,
right: 8,
),
child: RuleTitle(
profileId: widget.profileId,
),
),
),
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 0),
sliver: RuleContent(
maxWidth: _currentMaxWidth,
ruleListKey: _ruleListKey,
),
),
SliverToBoxAdapter(
child: SizedBox(
height: 16,
),
),
],
),
);
},
),
);
}
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
profileOverrideStateProvider.overrideWith(() => ProfileOverrideState()),
],
child: Consumer(
builder: (_, ref, child) {
_initState(ref);
return child!;
},
child: Consumer(
builder: (_, ref, ___) {
final vm2 = ref.watch(
profileOverrideStateProvider.select(
(state) => VM2(
a: state.isEdit,
b: state.selectedRules.length,
),
),
);
final isEdit = vm2.a;
final editCount = vm2.b;
return CommonScaffold(
title: appLocalizations.override,
body: _buildContent(),
actions: [
if (!isEdit)
Consumer(
builder: (_, ref, child) {
final overrideData = ref.watch(
getProfileOverrideDataProvider(widget.profileId));
final newOverrideData = ref.watch(
profileOverrideStateProvider.select(
(state) => state.overrideData,
),
);
final equals = overrideData == newOverrideData;
if (equals || newOverrideData == null) {
return SizedBox();
}
return CommonPopScope(
onPop: () async {
if (equals) {
return true;
}
final res = await globalState.showMessage(
message: TextSpan(
text: appLocalizations.saveChanges,
),
confirmText: appLocalizations.save,
);
if (!context.mounted || res != true) {
return true;
}
_handleSave(ref, newOverrideData);
return true;
},
child: IconButton(
onPressed: () async {
final res = await globalState.showMessage(
message: TextSpan(
text: appLocalizations.saveTip,
),
confirmText: appLocalizations.tip,
);
if (res != true) {
return;
}
_handleSave(ref, newOverrideData);
},
icon: Icon(
Icons.save,
),
),
);
},
),
if (editCount == 1)
IconButton(
onPressed: () {
final rule = ref.read(profileOverrideStateProvider.select(
(state) {
return state.overrideData?.rule.rules.firstWhere(
(item) => item.id == state.selectedRules.first,
);
},
));
if (rule == null) {
return;
}
globalState.appController.handleAddOrUpdate(
ref,
rule,
);
},
icon: Icon(
Icons.edit,
),
),
if (editCount > 0)
IconButton(
onPressed: () {
_handleDelete(ref);
},
icon: Icon(
Icons.delete,
),
)
],
appBarEditState: AppBarEditState(
isEdit: isEdit,
editCount: editCount,
onExit: () {
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith(
isEdit: false,
selectedRules: {},
),
);
},
),
);
},
),
),
);
}
}
class OverrideSwitch extends ConsumerWidget {
const OverrideSwitch({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final enable = ref.watch(
profileOverrideStateProvider.select(
(state) => state.overrideData?.enable,
),
);
return CommonCard(
onPressed: () {},
type: CommonCardType.filled,
radius: 18,
child: ListItem.switchItem(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 4,
bottom: 4,
),
title: Text(appLocalizations.enableOverride),
delegate: SwitchDelegate(
value: enable ?? false,
onChanged: (value) {
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith.overrideData!(
enable: value,
),
);
},
),
),
);
}
}
class RuleTitle extends ConsumerWidget {
final String profileId;
const RuleTitle({
super.key,
required this.profileId,
});
_handleChangeType(WidgetRef ref, isOverrideRule) {
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith.overrideData!.rule(
type: isOverrideRule
? OverrideRuleType.added
: OverrideRuleType.override,
),
);
}
@override
Widget build(BuildContext context, ref) {
final vm3 = ref.watch(
profileOverrideStateProvider.select(
(state) {
final overrideRule = state.overrideData?.rule;
return VM3(
a: state.isEdit,
b: state.selectedRules.containsAll(
overrideRule?.rules.map((item) => item.id).toSet() ?? {},
),
c: overrideRule?.type == OverrideRuleType.override,
);
},
),
);
final isEdit = vm3.a;
final isSelectAll = vm3.b;
final isOverrideRule = vm3.c;
return FilledButtonTheme(
data: FilledButtonThemeData(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.symmetric(
horizontal: 8,
)),
visualDensity: VisualDensity.compact,
),
),
child: IconButtonTheme(
data: IconButtonThemeData(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.zero),
visualDensity: VisualDensity.compact,
iconSize: WidgetStatePropertyAll(20),
),
),
child: ListHeader(
title: appLocalizations.rule,
subTitle: isOverrideRule
? appLocalizations.overrideOriginRules
: appLocalizations.addedOriginRules,
space: 8,
actions: [
if (!isEdit)
IconButton.filledTonal(
icon: Icon(
isOverrideRule ? Icons.edit_document : Icons.note_add,
),
onPressed: () {
_handleChangeType(
ref,
isOverrideRule,
);
},
),
!isEdit
? FilledButton.tonal(
onPressed: () {
globalState.appController.handleAddOrUpdate(ref);
},
child: Text(appLocalizations.add),
)
: isSelectAll
? FilledButton(
onPressed: () {
ref
.read(profileOverrideStateProvider.notifier)
.updateState(
(state) => state.copyWith(
selectedRules: {},
),
);
},
child: Text(appLocalizations.selectAll),
)
: FilledButton.tonal(
onPressed: () {
ref
.read(profileOverrideStateProvider.notifier)
.updateState(
(state) => state.copyWith(
selectedRules: state.overrideData?.rule.rules
.map((item) => item.id)
.toSet() ??
{},
),
);
},
child: Text(appLocalizations.selectAll),
),
],
),
),
);
}
}
class RuleContent extends ConsumerWidget {
final Key ruleListKey;
final double maxWidth;
const RuleContent({
super.key,
required this.ruleListKey,
required this.maxWidth,
});
Widget _proxyDecorator(
Widget child,
int index,
Animation<double> animation,
) {
return AnimatedBuilder(
animation: animation,
builder: (_, Widget? child) {
final double animValue = Curves.easeInOut.transform(animation.value);
final double scale = lerpDouble(1, 1.02, animValue)!;
return Transform.scale(
scale: scale,
child: child,
);
},
child: child,
);
}
Widget _buildItem(Rule rule, int index) {
return Consumer(
builder: (context, ref, ___) {
final vm2 = ref.watch(profileOverrideStateProvider.select(
(item) => VM2(
a: item.isEdit,
b: item.selectedRules.contains(rule.id),
),
));
final isEdit = vm2.a;
final isSelected = vm2.b;
return Material(
color: Colors.transparent,
child: Container(
margin: EdgeInsets.symmetric(
vertical: 4,
),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected
? context.colorScheme.secondaryContainer.opacity80
: context.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
clipBehavior: Clip.hardEdge,
child: ListTile(
minTileHeight: 0,
minVerticalPadding: 0,
titleTextStyle: context.textTheme.bodyMedium?.toJetBrainsMono,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
trailing: SizedBox(
width: 24,
height: 24,
child: !isEdit
? ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
)
: CommonCheckBox(
value: isSelected,
isCircle: true,
onChanged: (_) {
_handleSelect(ref, rule);
},
),
),
title: Text(rule.value),
),
),
);
},
);
}
_handleSelect(WidgetRef ref, ruleId) {
if (!ref.read(profileOverrideStateProvider).isEdit) {
return;
}
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) {
final newSelectedRules = Set<String>.from(state.selectedRules);
if (newSelectedRules.contains(ruleId)) {
newSelectedRules.remove(ruleId);
} else {
newSelectedRules.add(ruleId);
}
return state.copyWith(
selectedRules: newSelectedRules,
);
},
);
}
@override
Widget build(BuildContext context, ref) {
final vm2 = ref.watch(
profileOverrideStateProvider.select(
(state) {
final overrideRule = state.overrideData?.rule;
return VM2(
a: overrideRule?.rules ?? [],
b: overrideRule?.type ?? OverrideRuleType.added,
);
},
),
);
final rules = vm2.a;
final type = vm2.b;
if (rules.isEmpty) {
return SliverToBoxAdapter(
child: SizedBox(
height: 300,
child: Center(
child: type == OverrideRuleType.added
? Text(
appLocalizations.noData,
)
: FilledButton(
onPressed: () {
final rules = ref.read(
profileOverrideStateProvider.select(
(state) => state.snippet?.rule ?? [],
),
);
ref
.read(profileOverrideStateProvider.notifier)
.updateState(
(state) {
return state.copyWith.overrideData!.rule(
overrideRules: rules,
);
},
);
},
child: Text(appLocalizations.getOriginRules),
),
),
),
);
}
return CacheItemExtentSliverReorderableList(
key: ruleListKey,
itemBuilder: (context, index) {
final rule = rules[index];
return GestureDetector(
key: ObjectKey(rule),
child: _buildItem(
rule,
index,
),
onTap: () {
_handleSelect(ref, rule.id);
},
onLongPress: () {
if (ref.read(profileOverrideStateProvider).isEdit) {
return;
}
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith(
isEdit: true,
selectedRules: {
rule.id,
},
),
);
},
);
},
proxyDecorator: _proxyDecorator,
itemCount: rules.length,
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final newRules = List<Rule>.from(rules);
final item = newRules.removeAt(oldIndex);
newRules.insert(newIndex, item);
ref.read(profileOverrideStateProvider.notifier).updateState(
(state) => state.copyWith.overrideData!(
rule: state.overrideData!.rule.updateRules((_) => newRules),
),
);
},
keyBuilder: (int index) {
return rules[index].value;
},
itemExtentBuilder: (index) {
final rule = rules[index];
return 40 +
globalState.measure
.computeTextSize(
Text(
rule.value,
style: context.textTheme.bodyMedium?.toJetBrainsMono,
),
maxWidth: maxWidth,
)
.height;
},
);
}
}
class AddRuleDialog extends StatefulWidget {
final ClashConfigSnippet snippet;
final Rule? rule;
const AddRuleDialog({
super.key,
required this.snippet,
this.rule,
});
@override
State<AddRuleDialog> createState() => _AddRuleDialogState();
}
class _AddRuleDialogState extends State<AddRuleDialog> {
late RuleAction _ruleAction;
final _ruleTargetController = TextEditingController();
final _contentController = TextEditingController();
final _ruleProviderController = TextEditingController();
final _subRuleController = TextEditingController();
bool _noResolve = false;
bool _src = false;
List<DropdownMenuEntry> _targetItems = [];
List<DropdownMenuEntry> _ruleProviderItems = [];
List<DropdownMenuEntry> _subRuleItems = [];
final _formKey = GlobalKey<FormState>();
@override
void initState() {
_initState();
super.initState();
}
_initState() {
_targetItems = [
...widget.snippet.proxyGroups.map(
(item) => DropdownMenuEntry(
value: item.name,
label: item.name,
),
),
...RuleTarget.values.map(
(item) => DropdownMenuEntry(
value: item.name,
label: item.name,
),
),
];
_ruleProviderItems = [
...widget.snippet.ruleProvider.map(
(item) => DropdownMenuEntry(
value: item.name,
label: item.name,
),
),
];
_subRuleItems = [
...widget.snippet.subRules.map(
(item) => DropdownMenuEntry(
value: item.name,
label: item.name,
),
),
];
if (widget.rule != null) {
final parsedRule = ParsedRule.parseString(widget.rule!.value);
_ruleAction = parsedRule.ruleAction;
_contentController.text = parsedRule.content ?? "";
_ruleTargetController.text = parsedRule.ruleTarget ?? "";
_ruleProviderController.text = parsedRule.ruleProvider ?? "";
_subRuleController.text = parsedRule.subRule ?? "";
_noResolve = parsedRule.noResolve;
_src = parsedRule.src;
return;
}
_ruleAction = RuleAction.values.first;
if (_targetItems.isNotEmpty) {
_ruleTargetController.text = _targetItems.first.value;
}
if (_ruleProviderItems.isNotEmpty) {
_ruleProviderController.text = _ruleProviderItems.first.value;
}
if (_subRuleItems.isNotEmpty) {
_subRuleController.text = _subRuleItems.first.value;
}
}
@override
void didUpdateWidget(AddRuleDialog oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.rule != widget.rule) {
_initState();
}
}
_handleSubmit() {
final res = _formKey.currentState?.validate();
if (res == false) {
return;
}
final parsedRule = ParsedRule(
ruleAction: _ruleAction,
content: _contentController.text,
ruleProvider: _ruleProviderController.text,
ruleTarget: _ruleTargetController.text,
subRule: _subRuleController.text,
noResolve: _noResolve,
src: _src,
);
final rule = widget.rule != null
? widget.rule!.copyWith(value: parsedRule.value)
: Rule.value(
parsedRule.value,
);
Navigator.of(context).pop(rule);
}
@override
Widget build(BuildContext context) {
return CommonDialog(
title: appLocalizations.addRule,
actions: [
TextButton(
onPressed: _handleSubmit,
child: Text(
appLocalizations.confirm,
),
),
],
child: DropdownMenuTheme(
data: DropdownMenuThemeData(
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(),
labelStyle: context.textTheme.bodyLarge
?.copyWith(overflow: TextOverflow.ellipsis),
),
),
child: Form(
key: _formKey,
child: LayoutBuilder(
builder: (_, constraints) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FilledButton.tonal(
onPressed: () async {
_ruleAction =
await globalState.showCommonDialog<RuleAction>(
child: OptionsDialog<RuleAction>(
title: appLocalizations.ruleName,
options: RuleAction.values,
textBuilder: (item) => item.value,
value: _ruleAction,
),
) ??
_ruleAction;
setState(() {});
},
child: Text(_ruleAction.name),
),
SizedBox(
height: 24,
),
_ruleAction == RuleAction.RULE_SET
? FormField(
validator: (_) {
if (_ruleProviderController.text.isEmpty) {
return appLocalizations.ruleProviderEmptyTip;
}
return null;
},
builder: (field) {
return DropdownMenu(
expandedInsets: EdgeInsets.zero,
controller: _ruleProviderController,
label: Text(appLocalizations.ruleProviders),
menuHeight: 250,
errorText: field.errorText,
dropdownMenuEntries: _ruleProviderItems,
);
},
)
: TextFormField(
controller: _contentController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: appLocalizations.content,
),
validator: (_) {
if (_contentController.text.isEmpty) {
return appLocalizations.contentEmptyTip;
}
return null;
},
),
SizedBox(
height: 24,
),
_ruleAction == RuleAction.SUB_RULE
? FormField(
validator: (_) {
if (_subRuleController.text.isEmpty) {
return appLocalizations.subRuleEmptyTip;
}
return null;
},
builder: (filed) {
return DropdownMenu(
width: 200,
controller: _subRuleController,
label: Text(appLocalizations.subRule),
menuHeight: 250,
dropdownMenuEntries: _subRuleItems,
);
},
)
: FormField<String>(
validator: (_) {
if (_ruleTargetController.text.isEmpty) {
return appLocalizations.ruleTargetEmptyTip;
}
return null;
},
builder: (filed) {
return DropdownMenu(
controller: _ruleTargetController,
initialSelection: filed.value,
label: Text(appLocalizations.ruleTarget),
width: 200,
menuHeight: 250,
enableFilter: true,
dropdownMenuEntries: _targetItems,
errorText: filed.errorText,
);
},
),
if (_ruleAction.hasParams) ...[
SizedBox(
height: 20,
),
Wrap(
spacing: 8,
children: [
CommonCard(
radius: 8,
isSelected: _src,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 8, vertical: 8),
child: Text(
appLocalizations.sourceIp,
style: context.textTheme.bodyMedium,
),
),
onPressed: () {
setState(() {
_src = !_src;
});
},
),
CommonCard(
radius: 8,
isSelected: _noResolve,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 8, vertical: 8),
child: Text(
appLocalizations.noResolve,
style: context.textTheme.bodyMedium,
),
),
onPressed: () {
setState(() {
_noResolve = !_noResolve;
});
},
)
],
),
],
SizedBox(
height: 20,
),
],
);
},
),
),
),
);
}
}

View File

@@ -3,12 +3,13 @@ import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/fragments/profiles/override_profile.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'add_profile.dart';
@@ -19,35 +20,38 @@ class ProfilesFragment extends StatefulWidget {
State<ProfilesFragment> createState() => _ProfilesFragmentState();
}
class _ProfilesFragmentState extends State<ProfilesFragment> {
class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
Function? applyConfigDebounce;
_handleShowAddExtendPage() {
showExtendPage(
showExtend(
globalState.navigatorKey.currentState!.context,
body: AddProfile(
context: globalState.navigatorKey.currentState!.context,
),
title: "${appLocalizations.add}${appLocalizations.profile}",
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: AddProfile(
context: globalState.navigatorKey.currentState!.context,
),
title: "${appLocalizations.add}${appLocalizations.profile}",
);
},
);
}
_updateProfiles() async {
final appController = globalState.appController;
final config = appController.config;
final profiles = appController.config.profiles;
final profiles = globalState.config.profiles;
final messages = [];
final updateProfiles = profiles.map<Future>(
(profile) async {
if (profile.type == ProfileType.file) return;
config.setProfile(
globalState.appController.setProfile(
profile.copyWith(isUpdating: true),
);
try {
await appController.updateProfile(profile);
await globalState.appController.updateProfile(profile);
} catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n");
config.setProfile(
globalState.appController.setProfile(
profile.copyWith(
isUpdating: false,
),
@@ -70,96 +74,91 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
_initScaffold() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (!mounted) return;
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
_updateProfiles();
},
icon: const Icon(Icons.sync),
),
IconButton(
onPressed: () {
final profiles = globalState.appController.config.profiles;
showSheet(
title: appLocalizations.profilesSort,
context: context,
body: SizedBox(
height: 400,
child: ReorderableProfiles(profiles: profiles),
),
);
},
icon: const Icon(Icons.sort),
iconSize: 26,
),
];
commonScaffoldState?.floatingActionButton = FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
);
},
);
}
@override
List<Widget> get actions => [
IconButton(
onPressed: () {
_updateProfiles();
},
icon: const Icon(Icons.sync),
),
IconButton(
onPressed: () {
final profiles = globalState.config.profiles;
showSheet(
context: context,
builder: (_, type) {
return ReorderableProfilesSheet(
type: type,
profiles: profiles,
);
},
);
},
icon: const Icon(Icons.sort),
iconSize: 26,
),
];
@override
Widget? get floatingActionButton => FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
);
@override
Widget build(BuildContext context) {
return ActiveBuilder(
label: "profiles",
builder: (isCurrent, child) {
if (isCurrent) {
_initScaffold();
}
return child!;
},
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
columns: other.getProfilesColumns(appState.viewWidth),
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
),
return Consumer(
builder: (_, ref, __) {
ref.listenManual(
isCurrentPageProvider(PageLabel.profiles),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
final profilesSelectorState = ref.watch(profilesSelectorStateProvider);
if (profilesSelectorState.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
},
),
}
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: profilesSelectorState.columns,
children: [
for (int i = 0; i < profilesSelectorState.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(profilesSelectorState.profiles[i].id),
profile: profilesSelectorState.profiles[i],
groupValue: profilesSelectorState.currentProfileId,
onChanged: (profileId) {
ref.read(currentProfileIdProvider.notifier).value =
profileId;
},
),
),
],
),
),
);
},
);
}
}
@@ -189,24 +188,19 @@ class ProfileItem extends StatelessWidget {
await globalState.appController.deleteProfile(profile.id);
}
_handleUpdateProfile() async {
await globalState.safeRun<void>(updateProfile);
}
Future updateProfile() async {
final appController = globalState.appController;
final config = appController.config;
if (profile.type == ProfileType.file) return;
await globalState.safeRun(silence: false, () async {
try {
config.setProfile(
appController.setProfile(
profile.copyWith(
isUpdating: true,
),
);
await appController.updateProfile(profile);
} catch (e) {
config.setProfile(
appController.setProfile(
profile.copyWith(
isUpdating: false,
),
@@ -217,13 +211,18 @@ class ProfileItem extends StatelessWidget {
}
_handleShowEditExtendPage(BuildContext context) {
showExtendPage(
showExtend(
context,
body: EditProfile(
profile: profile,
context: context,
),
title: "${appLocalizations.edit}${appLocalizations.profile}",
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: EditProfile(
profile: profile,
context: context,
),
title: "${appLocalizations.edit}${appLocalizations.profile}",
);
},
);
}
@@ -256,16 +255,16 @@ class ProfileItem extends StatelessWidget {
];
}
_handleCopyLink(BuildContext context) async {
await Clipboard.setData(
ClipboardData(
text: profile.url,
),
);
if (context.mounted) {
context.showNotifier(appLocalizations.copySuccess);
}
}
// _handleCopyLink(BuildContext context) async {
// await Clipboard.setData(
// ClipboardData(
// text: profile.url,
// ),
// );
// if (context.mounted) {
// context.showNotifier(appLocalizations.copySuccess);
// }
// }
_handleExportFile(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState;
@@ -286,9 +285,18 @@ class ProfileItem extends StatelessWidget {
}
}
_handlePushGenProfilePage(BuildContext context, String id) {
BaseNavigator.push(
context,
OverrideProfile(
profileId: id,
),
);
}
@override
Widget build(BuildContext context) {
final key = GlobalKey<CommonPopupBoxState>();
final controller = PopupController();
return CommonCard(
isSelected: profile.id == groupValue,
onPressed: () {
@@ -301,17 +309,17 @@ class ProfileItem extends StatelessWidget {
trailing: SizedBox(
height: 40,
width: 40,
child: FadeBox(
child: FadeThroughBox(
child: profile.isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: CommonPopupBox(
key: key,
controller: controller,
popup: CommonPopupMenu(
items: [
ActionItemData(
PopupMenuItemData(
icon: Icons.edit_outlined,
label: appLocalizations.edit,
onPressed: () {
@@ -319,42 +327,42 @@ class ProfileItem extends StatelessWidget {
},
),
if (profile.type == ProfileType.url) ...[
ActionItemData(
PopupMenuItemData(
icon: Icons.sync_alt_sharp,
label: appLocalizations.sync,
onPressed: () {
_handleUpdateProfile();
},
),
ActionItemData(
icon: Icons.copy,
label: appLocalizations.copyLink,
onPressed: () {
_handleCopyLink(context);
updateProfile();
},
),
],
ActionItemData(
PopupMenuItemData(
icon: Icons.extension_outlined,
label: appLocalizations.override,
onPressed: () {
_handlePushGenProfilePage(context, profile.id);
},
),
PopupMenuItemData(
icon: Icons.file_copy_outlined,
label: appLocalizations.exportFile,
onPressed: () {
_handleExportFile(context);
},
),
ActionItemData(
PopupMenuItemData(
icon: Icons.delete_outlined,
iconSize: 20,
label: appLocalizations.delete,
onPressed: () {
_handleDeleteProfile(context);
},
type: ActionType.danger,
type: PopupMenuItemType.danger,
),
],
),
target: IconButton(
onPressed: () {
key.currentState?.pop();
controller.open();
},
icon: Icon(Icons.more_vert),
),
@@ -393,19 +401,22 @@ class ProfileItem extends StatelessWidget {
}
}
class ReorderableProfiles extends StatefulWidget {
class ReorderableProfilesSheet extends StatefulWidget {
final List<Profile> profiles;
final SheetType type;
const ReorderableProfiles({
const ReorderableProfilesSheet({
super.key,
required this.profiles,
required this.type,
});
@override
State<ReorderableProfiles> createState() => _ReorderableProfilesState();
State<ReorderableProfilesSheet> createState() =>
_ReorderableProfilesSheetState();
}
class _ReorderableProfilesState extends State<ReorderableProfiles> {
class _ReorderableProfilesSheetState extends State<ReorderableProfilesSheet> {
late List<Profile> profiles;
@override
@@ -449,74 +460,61 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 1,
child: ReorderableListView.builder(
buildDefaultDragHandles: false,
padding: const EdgeInsets.symmetric(horizontal: 12),
proxyDecorator: proxyDecorator,
onReorder: (oldIndex, newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final profile = profiles.removeAt(oldIndex);
profiles.insert(newIndex, profile);
});
},
itemBuilder: (_, index) {
final profile = profiles[index];
return Container(
key: Key(profile.id),
padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 16,
left: 16,
),
title: Text(profile.label ?? profile.id),
trailing: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
return AdaptiveSheetScaffold(
type: widget.type,
actions: [
IconButton(
onPressed: () {
Navigator.of(context).pop();
globalState.appController.setProfiles(profiles);
},
icon: Icon(
Icons.save,
),
)
],
body: Padding(
padding: EdgeInsets.only(bottom: 32),
child: ReorderableListView.builder(
buildDefaultDragHandles: false,
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
proxyDecorator: proxyDecorator,
onReorder: (oldIndex, newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final profile = profiles.removeAt(oldIndex);
profiles.insert(newIndex, profile);
});
},
itemBuilder: (_, index) {
final profile = profiles[index];
return Container(
key: Key(profile.id),
padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 16,
left: 16,
),
title: Text(profile.label ?? profile.id),
trailing: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
),
);
},
itemCount: profiles.length,
),
),
Container(
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 24,
),
child: FilledButton.tonal(
onPressed: () {
Navigator.of(context).pop();
globalState.appController.config.profiles = profiles;
},
style: ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(vertical: 8),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
appLocalizations.confirm,
),
],
),
),
);
},
itemCount: profiles.length,
),
],
),
title: appLocalizations.profilesSort,
);
}
}

View File

@@ -2,10 +2,11 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/proxies/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ProxyCard extends StatelessWidget {
final String groupName;
@@ -35,30 +36,28 @@ class ProxyCard extends StatelessWidget {
Widget _buildDelayText() {
return SizedBox(
height: measure.labelSmallHeight,
child: Selector<AppState, int?>(
selector: (context, appState) =>
globalState.appController.getDelay(proxy.name,testUrl),
builder: (context, delay, __) {
return FadeBox(
child: Builder(
builder: (_) {
if (delay == 0 || delay == null) {
return SizedBox(
height: measure.labelSmallHeight,
width: measure.labelSmallHeight,
child: delay == 0
? const CircularProgressIndicator(
strokeWidth: 2,
)
: IconButton(
icon: const Icon(Icons.bolt),
iconSize: globalState.measure.labelSmallHeight,
padding: EdgeInsets.zero,
onPressed: _handleTestCurrentDelay,
),
);
}
return GestureDetector(
child: Consumer(
builder: (context, ref, __) {
final delay = ref.watch(getDelayProvider(
proxyName: proxy.name,
testUrl: testUrl,
));
return delay == 0 || delay == null
? SizedBox(
height: measure.labelSmallHeight,
width: measure.labelSmallHeight,
child: delay == 0
? const CircularProgressIndicator(
strokeWidth: 2,
)
: IconButton(
icon: const Icon(Icons.bolt),
iconSize: globalState.measure.labelSmallHeight,
padding: EdgeInsets.zero,
onPressed: _handleTestCurrentDelay,
),
)
: GestureDetector(
onTap: _handleTestCurrentDelay,
child: Text(
delay > 0 ? '$delay ms' : "Timeout",
@@ -70,9 +69,6 @@ class ProxyCard extends StatelessWidget {
),
),
);
},
),
);
},
),
);
@@ -102,18 +98,17 @@ class ProxyCard extends StatelessWidget {
}
}
_changeProxy(BuildContext context) async {
final appController = globalState.appController;
final isURLTestOrFallback = groupType.isURLTestOrFallback;
_changeProxy(WidgetRef ref) async {
final isComputedSelected = groupType.isComputedSelected;
final isSelector = groupType == GroupType.Selector;
if (isURLTestOrFallback || isSelector) {
final currentProxyName =
appController.config.currentSelectedMap[groupName];
final nextProxyName = switch (isURLTestOrFallback) {
if (isComputedSelected || isSelector) {
final currentProxyName = ref.read(getProxyNameProvider(groupName));
final nextProxyName = switch (isComputedSelected) {
true => currentProxyName == proxy.name ? "" : proxy.name,
false => proxy.name,
};
appController.config.updateCurrentSelectedMap(
final appController = globalState.appController;
appController.updateCurrentSelectedMap(
groupName,
nextProxyName,
);
@@ -130,108 +125,136 @@ class ProxyCard extends StatelessWidget {
final measure = globalState.measure;
final delayText = _buildDelayText();
final proxyNameText = _buildProxyNameText(context);
return currentSelectedProxyNameBuilder(
groupName: groupName,
builder: (currentGroupName) {
return Stack(
children: [
CommonCard(
return Stack(
children: [
Consumer(
builder: (_, ref, child) {
final selectedProxyName =
ref.watch(getSelectedProxyNameProvider(groupName));
return CommonCard(
key: key,
enterAnimated: true,
onPressed: () {
_changeProxy(context);
_changeProxy(ref);
},
isSelected: currentGroupName == proxy.name,
child: Container(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
proxyNameText,
const SizedBox(
height: 8,
),
if (type == ProxyCardType.expand) ...[
SizedBox(
height: measure.bodySmallHeight,
child: Selector<AppState, String>(
selector: (context, appState) => appState.getDesc(
proxy.type,
proxy.name,
),
builder: (_, desc, __) {
return EmojiText(
desc,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color:
context.textTheme.bodySmall?.color?.toLight,
),
);
},
),
),
const SizedBox(
height: 8,
),
delayText,
] else
SizedBox(
height: measure.bodySmallHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
proxy.type,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: context
.textTheme.bodySmall?.color?.toLight,
),
),
),
),
delayText,
],
),
),
],
isSelected: selectedProxyName == proxy.name,
child: child!,
);
},
child: Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
proxyNameText,
const SizedBox(
height: 8,
),
),
),
if (groupType.isURLTestOrFallback)
Selector<Config, String>(
selector: (_, config) {
final selectedProxyName =
config.currentSelectedMap[groupName];
return selectedProxyName ?? '';
},
builder: (_, value, child) {
if (value != proxy.name) return Container();
return child!;
},
child: Positioned.fill(
child: Container(
alignment: Alignment.topRight,
margin: const EdgeInsets.all(8),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.secondaryContainer,
),
child: const SelectIcon(),
if (type == ProxyCardType.expand) ...[
SizedBox(
height: measure.bodySmallHeight,
child: _ProxyDesc(
proxy: proxy,
),
),
),
)
],
);
},
const SizedBox(
height: 6,
),
delayText,
] else
SizedBox(
height: measure.bodySmallHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
proxy.type,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color:
context.textTheme.bodySmall?.color?.opacity80,
),
),
),
),
delayText,
],
),
),
],
),
),
),
if (groupType.isComputedSelected)
Positioned(
top: 0,
right: 0,
child: _ProxyComputedMark(
groupName: groupName,
proxy: proxy,
),
)
],
);
}
}
class _ProxyDesc extends ConsumerWidget {
final Proxy proxy;
const _ProxyDesc({
required this.proxy,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final desc = ref.watch(
getProxyDescProvider(proxy),
);
return EmojiText(
desc,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color: context.textTheme.bodySmall?.color?.opacity80,
),
);
}
}
class _ProxyComputedMark extends ConsumerWidget {
final String groupName;
final Proxy proxy;
const _ProxyComputedMark({
required this.groupName,
required this.proxy,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final proxyName = ref.watch(
getProxyNameProvider(groupName),
);
if (proxyName != proxy.name) {
return SizedBox();
}
return Container(
alignment: Alignment.topRight,
margin: const EdgeInsets.all(8),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.secondaryContainer,
),
child: const SelectIcon(),
),
);
}
}

View File

@@ -3,24 +3,6 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Widget currentSelectedProxyNameBuilder({
required String groupName,
required Widget Function(String currentGroupName) builder,
}) {
return Selector2<AppState, Config, String>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName);
final selectedProxyName = config.currentSelectedMap[groupName];
return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? "";
},
builder: (_, currentSelectedProxyName, ___) {
return builder(currentSelectedProxyName);
},
);
}
double get listHeaderHeight {
final measure = globalState.measure;
@@ -30,9 +12,9 @@ double get listHeaderHeight {
double getItemHeight(ProxyCardType proxyCardType) {
final measure = globalState.measure;
final baseHeight =
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8 + 4;
16 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8 + 4;
return switch (proxyCardType) {
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 6,
ProxyCardType.shrink => baseHeight,
ProxyCardType.min => baseHeight - measure.bodyMediumHeight,
};
@@ -40,44 +22,52 @@ double getItemHeight(ProxyCardType proxyCardType) {
proxyDelayTest(Proxy proxy, [String? testUrl]) async {
final appController = globalState.appController;
final proxyName = appController.appState.getRealProxyName(proxy.name);
final url = appController.getRealTestUrl(testUrl);
globalState.appController.setDelay(
final state = appController.getProxyCardState(proxy.name);
final url = state.testUrl.getSafeValue(
appController.getRealTestUrl(testUrl),
);
if (state.proxyName.isEmpty) {
return;
}
appController.setDelay(
Delay(
url: url,
name: proxyName,
name: state.proxyName,
value: 0,
),
);
globalState.appController.setDelay(
appController.setDelay(
await clashCore.getDelay(
url,
proxyName,
state.proxyName,
),
);
}
delayTest(List<Proxy> proxies, [String? testUrl]) async {
final appController = globalState.appController;
final proxyNames = proxies
.map((proxy) => appController.appState.getRealProxyName(proxy.name))
.toSet()
.toList();
final url = appController.getRealTestUrl(testUrl);
final proxyNames = proxies.map((proxy) => proxy.name).toSet().toList();
final delayProxies = proxyNames.map<Future>((proxyName) async {
globalState.appController.setDelay(
final state = appController.getProxyCardState(proxyName);
final url = state.testUrl.getSafeValue(
appController.getRealTestUrl(testUrl),
);
final name = state.proxyName;
if (name.isEmpty) {
return;
}
appController.setDelay(
Delay(
url: url,
name: proxyName,
name: name,
value: 0,
),
);
globalState.appController.setDelay(
appController.setDelay(
await clashCore.getDelay(
url,
proxyName,
name,
),
);
}).toList();
@@ -86,7 +76,7 @@ delayTest(List<Proxy> proxies, [String? testUrl]) async {
for (final batchDelayProxies in batchesDelayProxies) {
await Future.wait(batchDelayProxies);
}
appController.appState.sortNum++;
appController.addSortNum();
}
double getScrollToSelectedOffset({
@@ -94,14 +84,11 @@ double getScrollToSelectedOffset({
required List<Proxy> proxies,
}) {
final appController = globalState.appController;
final columns = other.getProxiesColumns(
appController.appState.viewWidth,
appController.config.proxiesStyle.layout,
);
final proxyCardType = appController.config.proxiesStyle.cardType;
final selectedName = appController.getCurrentSelectedName(groupName);
final columns = appController.getProxiesColumns();
final proxyCardType = globalState.config.proxiesStyle.cardType;
final selectedProxyName = appController.getSelectedProxyName(groupName);
final findSelectedIndex = proxies.indexWhere(
(proxy) => proxy.name == selectedName,
(proxy) => proxy.name == selectedProxyName,
);
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
final rows = (selectedIndex / columns).floor();

View File

@@ -3,10 +3,13 @@ import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/providers/state.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'card.dart';
import 'common.dart';
@@ -78,7 +81,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
} else {
tempUnfoldSet.add(groupName);
}
globalState.appController.config.updateCurrentUnfoldSet(
globalState.appController.updateCurrentUnfoldSet(
tempUnfoldSet,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -105,7 +108,8 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
return itemHeightList;
}
List<Widget> _buildItems({
List<Widget> _buildItems(
WidgetRef ref, {
required List<String> groupNames,
required int columns,
required Set<String> currentUnfoldSet,
@@ -115,7 +119,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
final GroupNameProxiesMap groupNameProxiesMap = {};
for (final groupName in groupNames) {
final group =
globalState.appController.appState.getGroupWithName(groupName)!;
ref.read(groupsProvider.select((state) => state.getGroup(groupName)));
if (group == null) {
continue;
}
final isExpand = currentUnfoldSet.contains(groupName);
items.addAll([
ListHeader(
@@ -186,16 +193,21 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
return items;
}
_buildHeader({
_buildHeader(
WidgetRef ref, {
required String groupName,
required Set<String> currentUnfoldSet,
}) {
final group =
globalState.appController.appState.getGroupWithName(groupName)!;
ref.read(groupsProvider.select((state) => state.getGroup(groupName)));
if (group == null) {
return SizedBox();
}
final isExpand = currentUnfoldSet.contains(groupName);
return SizedBox(
height: listHeaderHeight,
child: ListHeader(
enterAnimated: false,
onScrollToSelected: _scrollToGroupSelected,
key: Key(groupName),
isExpand: isExpand,
@@ -212,7 +224,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
return;
}
final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups;
final currentGroups = appController.getCurrentGroups();
final groupNames = currentGroups.map((e) => e.name).toList();
final findIndex = groupNames.indexWhere((item) => item == groupName);
final index = findIndex != -1 ? findIndex : 0;
@@ -235,38 +247,16 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesListSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesListSelectorState(
groupNames: groupNames,
currentUnfoldSet: config.currentUnfoldSet,
proxyCardType: config.proxiesStyle.cardType,
proxiesSortType: config.proxiesStyle.sortType,
columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesStyle.layout,
),
sortNum: appState.sortNum,
);
},
shouldRebuild: (prev, next) {
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
_headerStateNotifier.value = const ProxiesListHeaderSelectorState(
offset: 0,
currentIndex: 0,
);
}
return prev != next;
},
builder: (_, state, __) {
return Consumer(
builder: (_, ref, __) {
final state = ref.watch(proxiesListSelectorStateProvider);
if (state.groupNames.isEmpty) {
return NullStatus(
label: appLocalizations.nullProxies,
);
}
final items = _buildItems(
ref,
groupNames: state.groupNames,
currentUnfoldSet: state.currentUnfoldSet,
columns: state.columns,
@@ -318,6 +308,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
bottom: 8,
),
child: _buildHeader(
ref,
groupName: state.groupNames[index],
currentUnfoldSet: state.currentUnfoldSet,
),
@@ -343,8 +334,11 @@ class ListHeader extends StatefulWidget {
final Function(String groupName) onScrollToSelected;
final bool isExpand;
final bool enterAnimated;
const ListHeader({
super.key,
this.enterAnimated = true,
required this.group,
required this.onChange,
required this.onScrollToSelected,
@@ -417,57 +411,53 @@ class _ListHeaderState extends State<ListHeader>
}
Widget _buildIcon() {
return Selector<Config, ProxiesIconStyle>(
selector: (_, config) => config.proxiesStyle.iconStyle,
builder: (_, iconStyle, child) {
return Selector<Config, String>(
selector: (_, config) {
final iconMapEntryList =
config.proxiesStyle.iconMap.entries.toList();
final index = iconMapEntryList.indexWhere((item) {
try {
return RegExp(item.key).hasMatch(groupName);
} catch (_) {
return false;
}
});
if (index != -1) {
return iconMapEntryList[index].value;
return Consumer(
builder: (_, ref, child) {
final iconStyle = ref.watch(
proxiesStyleSettingProvider.select((state) => state.iconStyle));
final icon = ref.watch(proxiesStyleSettingProvider.select((state) {
final iconMapEntryList = state.iconMap.entries.toList();
final index = iconMapEntryList.indexWhere((item) {
try {
return RegExp(item.key).hasMatch(groupName);
} catch (_) {
return false;
}
return icon;
},
builder: (_, icon, __) {
return switch (iconStyle) {
ProxiesIconStyle.standard => Container(
height: 48,
width: 48,
margin: const EdgeInsets.only(
right: 16,
),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: context.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
),
clipBehavior: Clip.antiAlias,
child: CommonTargetIcon(
src: icon,
size: 32,
),
),
ProxiesIconStyle.icon => Container(
margin: const EdgeInsets.only(
right: 16,
),
child: CommonTargetIcon(
src: icon,
size: 42,
),
),
ProxiesIconStyle.none => Container(),
};
},
);
});
if (index != -1) {
return iconMapEntryList[index].value;
}
return this.icon;
}));
return switch (iconStyle) {
ProxiesIconStyle.standard => Container(
height: 48,
width: 48,
margin: const EdgeInsets.only(
right: 16,
),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: context.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
),
clipBehavior: Clip.antiAlias,
child: CommonTargetIcon(
src: icon,
size: 32,
),
),
ProxiesIconStyle.icon => Container(
margin: const EdgeInsets.only(
right: 16,
),
child: CommonTargetIcon(
src: icon,
size: 42,
),
),
ProxiesIconStyle.none => Container(),
};
},
);
}
@@ -475,13 +465,11 @@ class _ListHeaderState extends State<ListHeader>
@override
Widget build(BuildContext context) {
return CommonCard(
enterAnimated: widget.enterAnimated,
key: widget.key,
backgroundColor: WidgetStatePropertyAll(
context.colorScheme.surfaceContainer,
),
radius: 14,
type: CommonCardType.filled,
child: Container(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
@@ -518,9 +506,13 @@ class _ListHeaderState extends State<ListHeader>
),
Flexible(
flex: 1,
child: currentSelectedProxyNameBuilder(
groupName: groupName,
builder: (currentGroupName) {
child: Consumer(
builder: (_, ref, __) {
final proxyName = ref
.watch(getSelectedProxyNameProvider(
groupName,
))
.getSafeValue("");
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment:
@@ -528,12 +520,12 @@ class _ListHeaderState extends State<ListHeader>
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
if (currentGroupName.isNotEmpty) ...[
if (proxyName.isNotEmpty) ...[
Flexible(
flex: 1,
child: EmojiText(
overflow: TextOverflow.ellipsis,
" · $currentGroupName",
" · $proxyName",
style: context.textTheme
.labelMedium?.toLight,
),
@@ -560,6 +552,7 @@ class _ListHeaderState extends State<ListHeader>
children: [
if (isExpand) ...[
IconButton(
visualDensity: VisualDensity.standard,
onPressed: () {
widget.onScrollToSelected(groupName);
},
@@ -569,12 +562,13 @@ class _ListHeaderState extends State<ListHeader>
),
IconButton(
onPressed: _delayTest,
visualDensity: VisualDensity.standard,
icon: const Icon(
Icons.network_ping,
),
),
const SizedBox(
width: 4,
width: 6,
),
],
AnimatedBuilder(

View File

@@ -3,52 +3,36 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.dart';
import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
typedef UpdatingMap = Map<String, bool>;
class Providers extends StatefulWidget {
const Providers({
class ProvidersView extends ConsumerStatefulWidget {
final SheetType type;
const ProvidersView({
super.key,
required this.type,
});
@override
State<Providers> createState() => _ProvidersState();
ConsumerState<ProvidersView> createState() => _ProvidersViewState();
}
class _ProvidersState extends State<Providers> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) {
globalState.appController.updateProviders();
context.commonScaffoldState?.actions = [
IconButton(
onPressed: () {
_updateProviders();
},
icon: const Icon(
Icons.sync,
),
)
];
},
);
}
class _ProvidersViewState extends ConsumerState<ProvidersView> {
_updateProviders() async {
final appState = globalState.appController.appState;
final providers = globalState.appController.appState.providers;
final providers = ref.read(providersProvider);
final providersNotifier = ref.read(providersProvider.notifier);
final messages = [];
final updateProviders = providers.map<Future>(
(provider) async {
appState.setProvider(
providersNotifier.setProvider(
provider.copyWith(isUpdating: true),
);
final message = await clashCore.updateExternalProvider(
@@ -57,7 +41,7 @@ class _ProvidersState extends State<Providers> {
if (message.isNotEmpty) {
messages.add("${provider.name}: $message \n");
}
appState.setProvider(
providersNotifier.setProvider(
await clashCore.getExternalProvider(provider.name),
);
},
@@ -83,34 +67,42 @@ class _ProvidersState extends State<Providers> {
@override
Widget build(BuildContext context) {
return Selector<AppState, List<ExternalProvider>>(
selector: (_, appState) => appState.providers,
builder: (_, providers, ___) {
final proxyProviders =
providers.where((item) => item.type == "Proxy").map(
(item) => ProviderItem(
provider: item,
),
);
final ruleProviders =
providers.where((item) => item.type == "Rule").map(
(item) => ProviderItem(
provider: item,
),
);
final proxySection = generateSection(
title: appLocalizations.proxyProviders,
items: proxyProviders,
final providers = ref.watch(providersProvider);
final proxyProviders = providers.where((item) => item.type == "Proxy").map(
(item) => ProviderItem(
provider: item,
),
);
final ruleSection = generateSection(
title: appLocalizations.ruleProviders,
items: ruleProviders,
final ruleProviders = providers.where((item) => item.type == "Rule").map(
(item) => ProviderItem(
provider: item,
),
);
return generateListView([
...proxySection,
...ruleSection,
]);
},
final proxySection = generateSection(
title: appLocalizations.proxyProviders,
items: proxyProviders,
);
final ruleSection = generateSection(
title: appLocalizations.ruleProviders,
items: ruleProviders,
);
return AdaptiveSheetScaffold(
actions: [
IconButton(
onPressed: () {
_updateProviders();
},
icon: const Icon(
Icons.sync,
),
)
],
type: widget.type,
body: generateListView([
...proxySection,
...ruleSection,
]),
title: appLocalizations.providers,
);
}
}
@@ -124,11 +116,11 @@ class ProviderItem extends StatelessWidget {
});
_handleUpdateProvider() async {
final appState = globalState.appController.appState;
final appController = globalState.appController;
if (provider.vehicleType != "HTTP") return;
await globalState.safeRun(
() async {
appState.setProvider(
appController.setProvider(
provider.copyWith(
isUpdating: true,
),
@@ -140,7 +132,7 @@ class ProviderItem extends StatelessWidget {
},
silence: false,
);
appState.setProvider(
appController.setProvider(
await clashCore.getExternalProvider(provider.name),
);
await globalState.appController.updateGroupsDebounce();
@@ -149,7 +141,6 @@ class ProviderItem extends StatelessWidget {
_handleSideLoadProvider() async {
await globalState.safeRun<void>(() async {
final platformFile = await picker.pickerFile();
final appState = globalState.appController.appState;
final bytes = platformFile?.bytes;
if (bytes == null || provider.path == null) return;
final file = await File(provider.path!).create(recursive: true);
@@ -160,7 +151,7 @@ class ProviderItem extends StatelessWidget {
data: utf8.decode(bytes),
);
if (message.isNotEmpty) throw message;
appState.setProvider(
globalState.appController.setProvider(
await clashCore.getExternalProvider(provider.name),
);
if (message.isNotEmpty) throw message;
@@ -229,7 +220,7 @@ class ProviderItem extends StatelessWidget {
trailing: SizedBox(
height: 48,
width: 48,
child: FadeBox(
child: FadeThroughBox(
child: provider.isUpdating
? const Padding(
padding: EdgeInsets.all(8),

View File

@@ -1,114 +1,86 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/proxies/list.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/fragments/proxies/providers.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'common.dart';
import 'setting.dart';
import 'tab.dart';
class ProxiesFragment extends StatefulWidget {
class ProxiesFragment extends ConsumerStatefulWidget {
const ProxiesFragment({super.key});
@override
State<ProxiesFragment> createState() => _ProxiesFragmentState();
ConsumerState<ProxiesFragment> createState() => _ProxiesFragmentState();
}
class _ProxiesFragmentState extends State<ProxiesFragment> {
class _ProxiesFragmentState extends ConsumerState<ProxiesFragment>
with PageMixin {
final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey();
bool _hasProviders = false;
bool _isTab = false;
_initActions(ProxiesType proxiesType, bool hasProvider) {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.commonScaffoldState?.actions = [
if (hasProvider) ...[
@override
get actions => [
if (_hasProviders)
IconButton(
onPressed: () {
showExtendPage(
isScaffold: true,
extendPageWidth: 360,
showExtend(
context,
body: const Providers(),
title: appLocalizations.providers,
builder: (_, type) {
return ProvidersView(
type: type,
);
},
);
},
icon: const Icon(
Icons.poll_outlined,
),
),
],
if (proxiesType == ProxiesType.tab) ...[
IconButton(
onPressed: () {
_proxiesTabKey.currentState?.scrollToGroupSelected();
},
icon: const Icon(
Icons.adjust_outlined,
),
),
] else ...[
IconButton(
onPressed: () {
showExtendPage(
context,
extendPageWidth: 360,
title: appLocalizations.iconConfiguration,
body: Selector<Config, Map<String, String>>(
selector: (_, config) => config.proxiesStyle.iconMap,
shouldRebuild: (prev, next) {
return !stringAndStringMapEntryIterableEquality.equals(
prev.entries,
next.entries,
);
},
builder: (_, iconMap, __) {
final entries = iconMap.entries.toList();
return ListPage(
title: appLocalizations.iconConfiguration,
items: entries,
keyLabel: appLocalizations.regExp,
valueLabel: appLocalizations.icon,
keyBuilder: (item) => Key(item.key),
titleBuilder: (item) => Text(item.key),
leadingBuilder: (item) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: CommonTargetIcon(
src: item.value,
size: 42,
),
),
subtitleBuilder: (item) => Text(
item.value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onChange: (entries) {
final config = globalState.appController.config;
config.proxiesStyle = config.proxiesStyle.copyWith(
iconMap: Map.fromEntries(entries),
);
},
);
},
_isTab
? IconButton(
onPressed: () {
_proxiesTabKey.currentState?.scrollToGroupSelected();
},
icon: const Icon(
Icons.adjust_outlined,
),
);
},
icon: const Icon(
Icons.style_outlined,
),
),
],
)
: IconButton(
onPressed: () {
showExtend(
context,
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: const _IconConfigView(),
title: appLocalizations.iconConfiguration,
);
},
);
},
icon: const Icon(
Icons.style_outlined,
),
),
IconButton(
onPressed: () {
showSheet(
title: appLocalizations.proxiesSetting,
context: context,
body: const ProxiesSetting(),
props: SheetProps(
isScrollControlled: true,
),
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: const ProxiesSetting(),
title: appLocalizations.proxiesSetting,
);
},
);
},
icon: const Icon(
@@ -116,28 +88,86 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
),
)
];
});
@override
get floatingActionButton => _isTab
? DelayTestButton(
onClick: () async {
await delayTest(
currentTabProxies,
currentTabTestUrl,
);
},
)
: null;
@override
void initState() {
ref.listenManual(
proxiesActionsStateProvider,
fireImmediately: true,
(prev, next) {
if (prev == next) {
return;
}
if (next.pageLabel == PageLabel.proxies) {
_hasProviders = next.hasProviders;
_isTab = next.type == ProxiesType.tab;
initPageState();
return;
}
},
);
super.initState();
}
@override
Widget build(BuildContext context) {
return Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesStyle.type,
builder: (_, proxiesType, __) {
return ProxiesActionsBuilder(
builder: (state, child) {
if (state.isCurrent) {
_initActions(proxiesType, state.hasProvider);
}
return child!;
},
child: switch (proxiesType) {
ProxiesType.tab => ProxiesTabFragment(
key: _proxiesTabKey,
final proxiesType =
ref.watch(proxiesStyleSettingProvider.select((state) => state.type));
return switch (proxiesType) {
ProxiesType.tab => ProxiesTabFragment(
key: _proxiesTabKey,
),
ProxiesType.list => const ProxiesListFragment(),
};
}
}
class _IconConfigView extends ConsumerWidget {
const _IconConfigView();
@override
Widget build(BuildContext context, WidgetRef ref) {
final iconMap =
ref.watch(proxiesStyleSettingProvider.select((state) => state.iconMap));
return MapInputPage(
title: appLocalizations.iconConfiguration,
map: iconMap,
keyLabel: appLocalizations.regExp,
valueLabel: appLocalizations.icon,
titleBuilder: (item) => Text(item.key),
leadingBuilder: (item) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: CommonTargetIcon(
src: item.value,
size: 42,
),
),
subtitleBuilder: (item) => Text(
item.value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onChange: (value) {
ref.read(proxiesStyleSettingProvider.notifier).updateState(
(state) => state.copyWith(
iconMap: value,
),
ProxiesType.list => const ProxiesListFragment(),
},
);
);
},
);
}

View File

@@ -1,11 +1,10 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class ProxiesSetting extends StatelessWidget {
const ProxiesSetting({super.key});
@@ -56,10 +55,11 @@ class ProxiesSetting extends StatelessWidget {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesStyle.type,
builder: (_, proxiesType, __) {
final config = globalState.appController.config;
child: Consumer(
builder: (_, ref, __) {
final proxiesType = ref.watch(proxiesStyleSettingProvider.select(
(state) => state.type,
));
return Wrap(
spacing: 16,
children: [
@@ -71,9 +71,13 @@ class ProxiesSetting extends StatelessWidget {
),
isSelected: proxiesType == item,
onPressed: () {
config.proxiesStyle = config.proxiesStyle.copyWith(
type: item,
);
ref
.read(proxiesStyleSettingProvider.notifier)
.updateState((state) {
return state.copyWith(
type: item,
);
});
},
)
],
@@ -92,10 +96,11 @@ class ProxiesSetting extends StatelessWidget {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesSortType>(
selector: (_, config) => config.proxiesStyle.sortType,
builder: (_, proxiesSortType, __) {
final config = globalState.appController.config;
child: Consumer(
builder: (_, ref, __) {
final sortType = ref.watch(proxiesStyleSettingProvider.select(
(state) => state.sortType,
));
return Wrap(
spacing: 16,
children: [
@@ -105,11 +110,15 @@ class ProxiesSetting extends StatelessWidget {
label: _getStringProxiesSortType(item),
iconData: _getIconWithProxiesSortType(item),
),
isSelected: proxiesSortType == item,
isSelected: sortType == item,
onPressed: () {
config.proxiesStyle = config.proxiesStyle.copyWith(
sortType: item,
);
ref
.read(proxiesStyleSettingProvider.notifier)
.updateState((state) {
return state.copyWith(
sortType: item,
);
});
},
),
],
@@ -128,21 +137,26 @@ class ProxiesSetting extends StatelessWidget {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxyCardType>(
selector: (_, config) => config.proxiesStyle.cardType,
builder: (_, proxyCardType, __) {
final config = globalState.appController.config;
child: Consumer(
builder: (_, ref, __) {
final cardType = ref.watch(proxiesStyleSettingProvider.select(
(state) => state.cardType,
));
return Wrap(
spacing: 16,
children: [
for (final item in ProxyCardType.values)
SettingTextCard(
Intl.message(item.name),
isSelected: item == proxyCardType,
isSelected: item == cardType,
onPressed: () {
config.proxiesStyle = config.proxiesStyle.copyWith(
cardType: item,
);
ref
.read(proxiesStyleSettingProvider.notifier)
.updateState((state) {
return state.copyWith(
cardType: item,
);
});
},
)
],
@@ -163,21 +177,26 @@ class ProxiesSetting extends StatelessWidget {
horizontal: 16,
),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesLayout>(
selector: (_, config) => config.proxiesStyle.layout,
builder: (_, proxiesLayout, __) {
final config = globalState.appController.config;
child: Consumer(
builder: (_, ref, __) {
final layout = ref.watch(proxiesStyleSettingProvider.select(
(state) => state.layout,
));
return Wrap(
spacing: 16,
children: [
for (final item in ProxiesLayout.values)
SettingTextCard(
getTextForProxiesLayout(item),
isSelected: item == proxiesLayout,
isSelected: item == layout,
onPressed: () {
config.proxiesStyle = config.proxiesStyle.copyWith(
layout: item,
);
ref
.watch(proxiesStyleSettingProvider.notifier)
.updateState((state) {
return state.copyWith(
layout: item,
);
});
},
)
],
@@ -196,9 +215,11 @@ class ProxiesSetting extends StatelessWidget {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesIconStyle>(
selector: (_, config) => config.proxiesStyle.iconStyle,
builder: (_, iconStyle, __) {
child: Consumer(
builder: (_, ref, __) {
final iconStyle = ref.watch(proxiesStyleSettingProvider.select(
(state) => state.iconStyle,
));
return Wrap(
spacing: 16,
children: [
@@ -207,10 +228,13 @@ class ProxiesSetting extends StatelessWidget {
_getTextWithProxiesIconStyle(item),
isSelected: iconStyle == item,
onPressed: () {
final config = globalState.appController.config;
config.proxiesStyle = config.proxiesStyle.copyWith(
iconStyle: item,
);
ref
.read(proxiesStyleSettingProvider.notifier)
.updateState((state) {
return state.copyWith(
iconStyle: item,
);
});
},
),
],
@@ -224,8 +248,8 @@ class ProxiesSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 32),
return SingleChildScrollView(
padding: EdgeInsets.only(bottom: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -234,16 +258,17 @@ class ProxiesSetting extends StatelessWidget {
..._buildSortSetting(),
..._buildLayoutSetting(),
..._buildSizeSetting(),
Selector<Config, bool>(
selector: (_, config) =>
config.proxiesStyle.type == ProxiesType.list,
builder: (_, value, child) {
if (value) {
Consumer(
builder: (_, ref, child) {
final isList = ref.watch(proxiesStyleSettingProvider
.select((state) => state.type == ProxiesType.list));
if (isList) {
return child!;
}
return Container();
},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildGroupStyleSetting(),

Some files were not shown because too many files have changed in this diff Show More