Compare commits

...

7 Commits

Author SHA1 Message Date
chen08209
0389b6eb29 Optimize proxy
Optimize delayed sorting performance

Add expansion panel proxies page

Support to adjust the proxy card size

Support to adjust proxies columns number
2024-06-26 16:04:30 +08:00
chen08209
8f22cbf746 Fix autoRun show issues
Fix Android 10 issues

Optimize ip show
2024-06-23 03:07:52 +08:00
chen08209
1fcc412770 Add intranet IP display
Add connections page

Add search in connections, requests

Add keyword search in connections, requests, logs

Add basic viewing editing capabilities

Optimize update profile
2024-06-22 13:52:20 +08:00
chen08209
afa1b4f424 Update version 2024-06-19 10:12:21 +08:00
chen08209
fa67940ec9 Fix the problem of excessive memory usage in traffic usage.
Add lightBlue theme color

Fix start unable to update profile issues
2024-06-19 10:10:41 +08:00
chen08209
90bb670442 Fix flashback caused by process 2024-06-17 15:49:16 +08:00
chen08209
05abf2d56d Add build version
Optimize quick start

Update system default option
2024-06-16 16:48:52 +08:00
78 changed files with 4196 additions and 1427 deletions

View File

@@ -62,7 +62,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.follow.clash" applicationId "com.follow.clash"
minSdkVersion 24 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 34
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName

View File

@@ -23,7 +23,6 @@
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:extractNativeLibs="true" android:extractNativeLibs="true"
android:networkSecurityConfig="@xml/network_security_config"
android:label="FlClash"> android:label="FlClash">
<activity <activity
android:name="com.follow.clash.MainActivity" android:name="com.follow.clash.MainActivity"

View File

@@ -15,6 +15,7 @@ enum class RunState {
class GlobalState { class GlobalState {
companion object { companion object {
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP) val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var runTime: Date? = null
var flutterEngine: FlutterEngine? = null var flutterEngine: FlutterEngine? = null
fun getCurrentTilePlugin(): TilePlugin? = fun getCurrentTilePlugin(): TilePlugin? =
flutterEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin? flutterEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?

View File

@@ -64,7 +64,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
toast!!.show() toast!!.show()
} }
@RequiresApi(Build.VERSION_CODES.Q)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"moveTaskToBack" -> { "moveTaskToBack" -> {
@@ -151,7 +150,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
val message = call.argument<String>("message") val message = call.argument<String>("message")
tip(message) tip(message)
result.success(true) result.success(true)
} }
else -> { else -> {

View File

@@ -25,6 +25,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import java.util.Date
class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
@@ -93,6 +94,10 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
} }
} }
"GetRunTimeStamp" -> {
result.success(GlobalState.runTime?.time)
}
"startForeground" -> { "startForeground" -> {
title = call.argument<String>("title") as String title = call.argument<String>("title") as String
content = call.argument<String>("content") as String content = call.argument<String>("content") as String
@@ -118,6 +123,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
if (GlobalState.runState.value == RunState.START) return; if (GlobalState.runState.value == RunState.START) return;
flClashVpnService?.start(port, props) flClashVpnService?.start(port, props)
GlobalState.runState.value = RunState.START GlobalState.runState.value = RunState.START
GlobalState.runTime = Date()
startAfter() startAfter()
} }
@@ -126,6 +132,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
flClashVpnService?.stop() flClashVpnService?.stop()
unbindService() unbindService()
GlobalState.runState.value = RunState.STOP; GlobalState.runState.value = RunState.STOP;
GlobalState.runTime = null;
} }
@SuppressLint("ForegroundServiceType") @SuppressLint("ForegroundServiceType")

View File

@@ -82,13 +82,13 @@ type Delay struct {
} }
type Process struct { type Process struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Metadata constant.Metadata `json:"metadata"` Metadata *constant.Metadata `json:"metadata"`
} }
type ProcessMapItem struct { type ProcessMapItem struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Value *string `json:"value"` Value string `json:"value"`
} }
type Now struct { type Now struct {
@@ -396,8 +396,8 @@ func applyConfig(isPatch bool) {
if isPatch { if isPatch {
patchConfig(cfg.General) patchConfig(cfg.General)
} else { } else {
runtime.GC()
executor.Shutdown() executor.Shutdown()
runtime.GC()
hub.UltraApplyConfig(cfg, true) hub.UltraApplyConfig(cfg, true)
} }
} }

View File

@@ -13,6 +13,7 @@ const (
Now MessageType = "now" Now MessageType = "now"
Process MessageType = "process" Process MessageType = "process"
Request MessageType = "request" Request MessageType = "request"
Run MessageType = "run"
) )
type Message struct { type Message struct {
@@ -20,11 +21,18 @@ type Message struct {
Data interface{} `json:"data"` Data interface{} `json:"data"`
} }
func (message *Message) Json() string { func (message *Message) Json() (string, error) {
data, _ := json.Marshal(message) data, err := json.Marshal(message)
return string(data) return string(data), err
} }
func SendMessage(message Message) { func SendMessage(message Message) {
SendToPort(*Port, message.Json()) if Port == nil {
return
}
s, err := message.Json()
if err != nil {
return
}
SendToPort(*Port, s)
} }

View File

@@ -188,6 +188,26 @@ func getTraffic() *C.char {
return C.CString(string(data)) return C.CString(string(data))
} }
//export getTotalTraffic
func getTotalTraffic() *C.char {
up, down := statistic.DefaultManager.Total()
traffic := map[string]int64{
"up": up,
"down": down,
}
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export resetTraffic
func resetTraffic() {
statistic.DefaultManager.ResetStatistic()
}
//export asyncTestDelay //export asyncTestDelay
func asyncTestDelay(s *C.char, port C.longlong) { func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port) i := int64(port)

View File

@@ -1,171 +0,0 @@
//go:build android
package platform
import (
"bufio"
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"os"
"strconv"
"strings"
"unsafe"
)
var netIndexOfLocal = -1
var netIndexOfUid = -1
var nativeEndian binary.ByteOrder
func QuerySocketUidFromProcFs(source, _ net.Addr) int {
if netIndexOfLocal < 0 || netIndexOfUid < 0 {
return -1
}
network := source.Network()
if strings.HasSuffix(network, "4") || strings.HasSuffix(network, "6") {
network = network[:len(network)-1]
}
path := "/proc/net/" + network
var sIP net.IP
var sPort int
switch s := source.(type) {
case *net.TCPAddr:
sIP = s.IP
sPort = s.Port
case *net.UDPAddr:
sIP = s.IP
sPort = s.Port
default:
return -1
}
sIP = sIP.To16()
if sIP == nil {
return -1
}
uid := doQuery(path+"6", sIP, sPort)
if uid == -1 {
sIP = sIP.To4()
if sIP == nil {
return -1
}
uid = doQuery(path, sIP, sPort)
}
return uid
}
func doQuery(path string, sIP net.IP, sPort int) int {
file, err := os.Open(path)
if err != nil {
return -1
}
defer file.Close()
reader := bufio.NewReader(file)
var bytes [2]byte
binary.BigEndian.PutUint16(bytes[:], uint16(sPort))
local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:]))
for {
row, _, err := reader.ReadLine()
if err != nil {
return -1
}
fields := strings.Fields(string(row))
if len(fields) <= netIndexOfLocal || len(fields) <= netIndexOfUid {
continue
}
if strings.EqualFold(local, fields[netIndexOfLocal]) {
uid, err := strconv.Atoi(fields[netIndexOfUid])
if err != nil {
return -1
}
return uid
}
}
}
func nativeEndianIP(ip net.IP) []byte {
result := make([]byte, len(ip))
for i := 0; i < len(ip); i += 4 {
value := binary.BigEndian.Uint32(ip[i:])
nativeEndian.PutUint32(result[i:], value)
}
return result
}
func init() {
file, err := os.Open("/proc/net/tcp")
if err != nil {
return
}
defer file.Close()
reader := bufio.NewReader(file)
header, _, err := reader.ReadLine()
if err != nil {
return
}
columns := strings.Fields(string(header))
var txQueue, rxQueue, tr, tmWhen bool
for idx, col := range columns {
offset := 0
if txQueue && rxQueue {
offset--
}
if tr && tmWhen {
offset--
}
switch col {
case "tx_queue":
txQueue = true
case "rx_queue":
rxQueue = true
case "tr":
tr = true
case "tm->when":
tmWhen = true
case "local_address":
netIndexOfLocal = idx + offset
case "uid":
netIndexOfUid = idx + offset
}
}
}
func init() {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
nativeEndian = binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
}
}

View File

@@ -9,16 +9,30 @@ import (
"errors" "errors"
"github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
) )
var ( type ProcessMap struct {
counter int64 m sync.Map
) }
var processMap = make(map[int64]*string) func (cm *ProcessMap) Store(key int64, value string) {
cm.m.Store(key, value)
}
func (cm *ProcessMap) Load(key int64) (string, bool) {
value, ok := cm.m.Load(key)
if !ok || value == nil {
return "", false
}
return value.(string), true
}
var counter int64 = 0
var processMap ProcessMap
func init() { func init() {
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) { process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
@@ -29,31 +43,24 @@ func init() {
timeout := time.After(200 * time.Millisecond) timeout := time.After(200 * time.Millisecond)
message := &bridge.Message{ bridge.SendMessage(bridge.Message{
Type: bridge.Process, Type: bridge.Process,
Data: Process{ Data: Process{
Id: id, Id: id,
Metadata: *metadata, Metadata: metadata,
}, },
} })
bridge.SendMessage(*message)
for { for {
select { select {
case <-timeout: case <-timeout:
return "", errors.New("package resolver timeout") return "", errors.New("package resolver timeout")
default: default:
value, exists := processMap[counter] value, exists := processMap.Load(id)
if exists { if exists {
if value != nil { return value, nil
log.Infoln("[PKG] %s --> %s by [%s]", metadata.SourceAddress(), metadata.RemoteAddress(), *value)
return *value, nil
} else {
return "", process.ErrInvalidNetwork
}
} }
time.Sleep(10 * time.Millisecond) time.Sleep(20 * time.Millisecond)
} }
} }
} }
@@ -61,12 +68,15 @@ func init() {
//export setProcessMap //export setProcessMap
func setProcessMap(s *C.char) { func setProcessMap(s *C.char) {
if s == nil {
return
}
go func() { go func() {
paramsString := C.GoString(s) paramsString := C.GoString(s)
var processMapItem = &ProcessMapItem{} var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(paramsString), processMapItem) err := json.Unmarshal([]byte(paramsString), processMapItem)
if err == nil { if err == nil {
processMap[processMapItem.Id] = processMapItem.Value processMap.Store(processMapItem.Id, processMapItem.Value)
} }
}() }()
} }

View File

@@ -10,7 +10,6 @@ import (
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore" "golang.org/x/sync/semaphore"
"strconv"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@@ -18,16 +17,11 @@ import (
var tunLock sync.Mutex var tunLock sync.Mutex
var tun *t.Tun var tun *t.Tun
var runTime *time.Time
//export startTUN //export startTUN
func startTUN(fd C.int) { func startTUN(fd C.int) {
tunLock.Lock()
now := time.Now()
runTime = &now
go func() { go func() {
tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
if tun != nil { if tun != nil {
@@ -51,26 +45,13 @@ func startTUN(fd C.int) {
tempTun.Closer = closer tempTun.Closer = closer
tun = tempTun tun = tempTun
applyConfig(true)
}() }()
} }
//export getRunTime
func getRunTime() *C.char {
if runTime == nil {
return C.CString("")
}
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10))
}
//export stopTun //export stopTun
func stopTun() { func stopTun() {
tunLock.Lock()
runTime = nil
go func() { go func() {
tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
if tun != nil { if tun != nil {
@@ -80,10 +61,12 @@ func stopTun() {
}() }()
} }
var errBlocked = errors.New("blocked")
func init() { func init() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error { dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() { if platform.ShouldBlockConnection() {
return errors.New("blocked") return errBlocked
} }
return conn.Control(func(fd uintptr) { return conn.Control(func(fd uintptr) {
if tun != nil { if tun != nil {

View File

@@ -130,7 +130,6 @@ class ApplicationState extends State<Application> {
httpTimeoutDuration, httpTimeoutDuration,
(timer) async { (timer) async {
await globalState.appController.updateGroups(); await globalState.appController.updateGroups();
globalState.appController.appState.sortNum++;
}, },
); );
} }

View File

@@ -46,8 +46,8 @@ class ClashCore {
bool init(String homeDir) { bool init(String homeDir) {
return clashFFI.initClash( return clashFFI.initClash(
homeDir.toNativeUtf8().cast(), homeDir.toNativeUtf8().cast(),
) == ) ==
1; 1;
} }
@@ -95,12 +95,15 @@ class ClashCore {
final proxiesRaw = clashFFI.getProxies(); final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString(); final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
return Isolate.run<List<Group>>(() { return Isolate.run<List<Group>>(() {
final proxies = json.decode(proxiesRawString); if(proxiesRawString.isEmpty) return [];
final proxies = json.decode(proxiesRawString) as Map;
if(proxies.isEmpty) return [];
final groupNames = [ final groupNames = [
UsedProxy.GLOBAL.name, UsedProxy.GLOBAL.name,
...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) { ...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) {
final proxy = proxies[e]; final proxy = proxies[e] ?? {};
return GroupTypeExtension.valueList.contains(proxy['type']) && proxy['hidden'] != true; return GroupTypeExtension.valueList.contains(proxy['type']) &&
proxy['hidden'] != true;
}) })
]; ];
final groupsRaw = groupNames.map((groupName) { final groupsRaw = groupNames.map((groupName) {
@@ -108,7 +111,7 @@ class ClashCore {
group["all"] = ((group["all"] ?? []) as List) group["all"] = ((group["all"] ?? []) as List)
.map( .map(
(name) => proxies[name], (name) => proxies[name],
) )
.toList(); .toList();
return group; return group;
}).toList(); }).toList();
@@ -119,14 +122,14 @@ class ClashCore {
Future<List<ExternalProvider>> getExternalProviders() { Future<List<ExternalProvider>> getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders(); final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString = final externalProvidersRawString =
externalProvidersRaw.cast<Utf8>().toDartString(); externalProvidersRaw.cast<Utf8>().toDartString();
return Isolate.run<List<ExternalProvider>>(() { return Isolate.run<List<ExternalProvider>>(() {
final externalProviders = final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>) (json.decode(externalProvidersRawString) as List<dynamic>)
.map( .map(
(item) => ExternalProvider.fromJson(item), (item) => ExternalProvider.fromJson(item),
) )
.toList(); .toList();
return externalProviders; return externalProviders;
}); });
} }
@@ -175,9 +178,11 @@ class ClashCore {
); );
Future.delayed(httpTimeoutDuration + moreDuration, () { Future.delayed(httpTimeoutDuration + moreDuration, () {
receiver.close(); receiver.close();
completer.complete( if(!completer.isCompleted){
Delay(name: proxyName, value: -1), completer.complete(
); Delay(name: proxyName, value: -1),
);
}
}); });
return completer.future; return completer.future;
} }
@@ -198,6 +203,16 @@ class ClashCore {
return Traffic.fromMap(trafficMap); return Traffic.fromMap(trafficMap);
} }
Traffic getTotalTraffic() {
final trafficRaw = clashFFI.getTotalTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
return Traffic.fromMap(trafficMap);
}
void resetTraffic(){
clashFFI.resetTraffic();
}
void startLog() { void startLog() {
clashFFI.startLog(); clashFFI.startLog();
} }
@@ -222,19 +237,23 @@ class ClashCore {
clashFFI.setProcessMap(json.encode(processMapItem).toNativeUtf8().cast()); clashFFI.setProcessMap(json.encode(processMapItem).toNativeUtf8().cast());
} }
DateTime? getRunTime() { // DateTime? getRunTime() {
final runTimeString = clashFFI.getRunTime().cast<Utf8>().toDartString(); // final runTimeString = clashFFI.getRunTime().cast<Utf8>().toDartString();
if (runTimeString.isEmpty) return null; // if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); // return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
} // }
List<Connection> getConnections() { List<Connection> getConnections() {
final connectionsDataRaw = clashFFI.getConnections(); final connectionsDataRaw = clashFFI.getConnections();
final connectionsData = final connectionsData =
json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map; json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map;
final connectionsRaw = connectionsData['connections'] as List? ?? []; final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => Connection.fromJson(e)).toList(); return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
} }
closeConnections(String id) {
clashFFI.closeConnection(id.toNativeUtf8().cast());
}
} }
final clashCore = ClashCore(); final clashCore = ClashCore();

View File

@@ -983,6 +983,24 @@ class ClashFFI {
late final _getTraffic = late final _getTraffic =
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>(); _getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getTotalTraffic() {
return _getTotalTraffic();
}
late final _getTotalTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getTotalTraffic');
late final _getTotalTraffic =
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void resetTraffic() {
return _resetTraffic();
}
late final _resetTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('resetTraffic');
late final _resetTraffic = _resetTrafficPtr.asFunction<void Function()>();
void asyncTestDelay( void asyncTestDelay(
ffi.Pointer<ffi.Char> s, ffi.Pointer<ffi.Char> s,
int port, int port,
@@ -1156,16 +1174,6 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>('startTUN'); _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>('startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int)>(); late final _startTUN = _startTUNPtr.asFunction<void Function(int)>();
ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime();
}
late final _getRunTimePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getRunTime');
late final _getRunTime =
_getRunTimePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void stopTun() { void stopTun() {
return _stopTun(); return _stopTun();
} }

View File

@@ -19,6 +19,8 @@ abstract mixin class ClashMessageListener {
void onRequest(Connection connection) {} void onRequest(Connection connection) {}
void onNow(Now now) {} void onNow(Now now) {}
void onRun(String runTime) {}
} }
class ClashMessage { class ClashMessage {
@@ -51,6 +53,9 @@ class ClashMessage {
case MessageType.request: case MessageType.request:
listener.onRequest(Connection.fromJson(m.data)); listener.onRequest(Connection.fromJson(m.data));
break; break;
case MessageType.run:
listener.onRun(m.data);
break;
} }
} }
}); });

View File

@@ -7,6 +7,7 @@ const coreName = "clash.meta";
const packageName = "FlClash"; const packageName = "FlClash";
const httpTimeoutDuration = Duration(milliseconds: 5000); const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100); const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
const defaultUpdateDuration = Duration(days: 1); const defaultUpdateDuration = Duration(days: 1);
const mmdbFileName = "geoip.metadb"; const mmdbFileName = "geoip.metadb";
const geoSiteFileName = "GeoSite.dat"; const geoSiteFileName = "GeoSite.dat";

View File

@@ -30,12 +30,19 @@ class Navigation {
fragment: ProfilesFragment(), fragment: ProfilesFragment(),
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.ballot), icon: Icon(Icons.view_timeline),
label: "requests", label: "requests",
fragment: RequestFragment(), fragment: RequestsFragment(),
description: "requestsDesc", description: "requestsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem(
icon: Icon(Icons.ballot),
label: "connections",
fragment: ConnectionsFragment(),
description: "connectionsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.swap_vert_circle), icon: Icon(Icons.swap_vert_circle),
label: "resources", label: "resources",

View File

@@ -8,12 +8,30 @@ import 'constant.dart';
class AppPath { class AppPath {
static AppPath? _instance; static AppPath? _instance;
Completer<Directory> applicationSupportDirectoryCompleter = Completer(); Completer<Directory> cacheDir = Completer();
// Future<Directory> _createDesktopCacheDir() async {
// final path = join(dirname(Platform.resolvedExecutable), 'cache');
// final dir = Directory(path);
// if (await dir.exists()) {
// await dir.create(recursive: true);
// }
// return dir;
// }
AppPath._internal() { AppPath._internal() {
getApplicationSupportDirectory().then( getApplicationSupportDirectory().then((value) {
(value) => applicationSupportDirectoryCompleter.complete(value), cacheDir.complete(value);
); });
// if (Platform.isAndroid) {
// getApplicationSupportDirectory().then((value) {
// cacheDir.complete(value);
// });
// } else {
// _createDesktopCacheDir().then((value) {
// cacheDir.complete(value);
// });
// }
} }
factory AppPath() { factory AppPath() {
@@ -22,12 +40,12 @@ class AppPath {
} }
Future<String> getHomeDirPath() async { Future<String> getHomeDirPath() async {
final directory = await applicationSupportDirectoryCompleter.future; final directory = await cacheDir.future;
return directory.path; return directory.path;
} }
Future<String> getProfilesPath() async { Future<String> getProfilesPath() async {
final directory = await applicationSupportDirectoryCompleter.future; final directory = await cacheDir.future;
return join(directory.path, profilesDirectoryName); return join(directory.path, profilesDirectoryName);
} }

View File

@@ -2,20 +2,13 @@ import 'package:flutter/material.dart';
import 'color.dart'; import 'color.dart';
extension TextStyleExtension on TextStyle { extension TextStyleExtension on TextStyle {
toLight() { TextStyle get toLight => copyWith(color: color?.toLight());
return copyWith(color: color?.toLight());
}
toLighter() { TextStyle get toLighter => copyWith(color: color?.toLighter());
return copyWith(color: color?.toLighter());
}
TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500);
toSoftBold() { TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
return copyWith(fontWeight: FontWeight.w500);
}
toBold() { TextStyle get toMinus => copyWith(fontSize: fontSize! - 1);
return copyWith(fontWeight: FontWeight.bold); }
}
}

View File

@@ -31,6 +31,7 @@ class AppController {
Future<void> updateSystemProxy(bool isStart) async { Future<void> updateSystemProxy(bool isStart) async {
if (isStart) { if (isStart) {
await globalState.startSystemProxy( await globalState.startSystemProxy(
appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
@@ -42,7 +43,9 @@ class AppController {
]; ];
} else { } else {
await globalState.stopSystemProxy(); await globalState.stopSystemProxy();
clashCore.resetTraffic();
appState.traffics = []; appState.traffics = [];
appState.totalTraffic = Traffic();
appState.runTime = null; appState.runTime = null;
} }
} }
@@ -97,13 +100,9 @@ class AppController {
} }
} }
Future<void> updateProfile(String id) async { Future<void> updateProfile(Profile profile) async {
final profile = config.getCurrentProfileForId(id); await profile.update();
if (profile != null) { config.setProfile(await profile.update());
final tempProfile = profile.copyWith();
await tempProfile.update();
config.setProfile(tempProfile);
}
} }
Future<void> updateClashConfig({bool isPatch = true}) async { Future<void> updateClashConfig({bool isPatch = true}) async {
@@ -146,7 +145,7 @@ class AppController {
continue; continue;
} }
try { try {
await updateProfile(profile.id); updateProfile(profile);
} catch (e) { } catch (e) {
appState.addLog( appState.addLog(
Log( Log(
@@ -163,7 +162,7 @@ class AppController {
if (profile.type == ProfileType.file) { if (profile.type == ProfileType.file) {
continue; continue;
} }
await updateProfile(profile.id); await updateProfile(profile);
} }
} }
@@ -267,6 +266,7 @@ class AppController {
} }
init() async { init() async {
updateLogStatus();
if (!config.silentLaunch) { if (!config.silentLaunch) {
window?.show(); window?.show();
} }
@@ -278,7 +278,7 @@ class AppController {
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
}); }, title: appLocalizations.init);
} else { } else {
await globalState.applyProfile( await globalState.applyProfile(
appState: appState, appState: appState,
@@ -290,14 +290,13 @@ class AppController {
} }
afterInit() async { afterInit() async {
if (config.autoRun) { await proxyManager.updateStartTime();
if (proxyManager.isStart) {
await updateSystemProxy(true); await updateSystemProxy(true);
} else { } else {
await proxyManager.updateStartTime(); await updateSystemProxy(config.autoRun);
await updateSystemProxy(proxyManager.isStart);
} }
autoUpdateProfiles(); autoUpdateProfiles();
updateLogStatus();
autoCheckUpdate(); autoCheckUpdate();
} }
@@ -366,11 +365,9 @@ class AppController {
if (commonScaffoldState?.mounted != true) return; if (commonScaffoldState?.mounted != true) return;
final profile = await commonScaffoldState?.loadingRun<Profile>( final profile = await commonScaffoldState?.loadingRun<Profile>(
() async { () async {
final profile = Profile( return await Profile.normal(
url: url, url: url,
); ).update();
await profile.update();
return profile;
}, },
title: "${appLocalizations.add}${appLocalizations.profile}", title: "${appLocalizations.add}${appLocalizations.profile}",
); );
@@ -393,9 +390,7 @@ class AppController {
if (bytes == null) { if (bytes == null) {
return null; return null;
} }
final profile = Profile(label: platformFile?.name); return await Profile.normal(label: platformFile?.name).saveFile(bytes);
await profile.saveFile(bytes);
return profile;
}, },
title: "${appLocalizations.add}${appLocalizations.profile}", title: "${appLocalizations.add}${appLocalizations.profile}",
); );
@@ -410,9 +405,55 @@ class AppController {
addProfileFormURL(url); addProfileFormURL(url);
} }
int get columns =>
globalState.getColumns(appState.viewMode, config.proxiesColumns);
changeColumns() {
config.proxiesColumns = globalState.getColumns(
appState.viewMode,
columns - 1,
);
}
updateViewWidth(double width) { updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width; appState.viewWidth = width;
}); });
} }
List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies)
..sort(
(a, b) => other.sortByChar(a.name, b.name),
);
}
List<Proxy> _sortOfDelay(List<Proxy> proxies) {
return proxies = List.of(proxies)
..sort(
(a, b) {
final aDelay = appState.getDelay(a.name);
final bDelay = appState.getDelay(b.name);
if (aDelay == null && bDelay == null) {
return 0;
}
if (aDelay == null || aDelay == -1) {
return 1;
}
if (bDelay == null || bDelay == -1) {
return -1;
}
return aDelay.compareTo(bDelay);
},
);
}
List<Proxy> getSortProxies(List<Proxy> proxies){
return switch(config.proxiesSortType){
ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay(proxies),
ProxiesSortType.name =>_sortOfName(proxies),
};
}
} }

View File

@@ -56,7 +56,7 @@ enum ProfileType { file, url }
enum ResultType { success, error } enum ResultType { success, error }
enum MessageType { log, tun, delay, process, now, request } enum MessageType { log, tun, delay, process, now, request, run }
enum FindProcessMode { always, off } enum FindProcessMode { always, off }
@@ -64,3 +64,11 @@ enum RecoveryOption {
all, all,
onlyProfiles, onlyProfiles,
} }
enum ChipType { action, delete }
enum CommonCardType { plain, filled }
enum ProxiesType { tab, expansion }
enum ProxyCardType { expand, shrink }

View File

@@ -35,6 +35,13 @@ class _AccessFragmentState extends State<AccessFragment> {
}); });
} }
@override
void dispose() {
super.dispose();
packagesListenable.dispose();
}
Widget _buildAppProxyModePopup() { Widget _buildAppProxyModePopup() {
final items = [ final items = [
CommonPopupMenuItem( CommonPopupMenuItem(

View File

@@ -228,6 +228,13 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
Navigator.pop(context); Navigator.pop(context);
} }
@override
void dispose() {
super.dispose();
_obscureController.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(

View File

@@ -39,7 +39,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
} }
} }
_buildAppSection() { Widget _buildAppSection() {
final items = [ final items = [
if (Platform.isAndroid) if (Platform.isAndroid)
Selector<Config, bool>( Selector<Config, bool>(
@@ -150,7 +150,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
); );
} }
_buildGeneralSection() { Widget _buildGeneralSection() {
final items = [ final items = [
Selector<ClashConfig, LogLevel>( Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel, selector: (_, clashConfig) => clashConfig.logLevel,
@@ -191,7 +191,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
builder: (_, ipv6, __) { builder: (_, ipv6, __) {
return ListItem.switchItem( return ListItem.switchItem(
leading: const Icon(Icons.water_outlined), leading: const Icon(Icons.water_outlined),
title: const Text("Ipv6"), title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6Desc), subtitle: Text(appLocalizations.ipv6Desc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: ipv6, value: ipv6,
@@ -335,14 +335,56 @@ class _ConfigFragmentState extends State<ConfigFragment> {
); );
} }
Widget _buildMoreSection() {
final items = [
if (false)
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, tunEnable, __) {
return ListItem.switchItem(
leading: const Icon(
Icons.important_devices_outlined
),
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate(
value: tunEnable,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.tun = Tun(enable: value);
globalState.appController.updateClashConfigDebounce();
},
),
);
},
),
];
if(items.isEmpty) return Container();
return Section(
title: appLocalizations.general,
child: Column(
children: [
for (final item in items) ...[
item,
if (items.last != item)
const Divider(
height: 0,
)
]
],
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> items = [ List<Widget> items = [
_buildAppSection(), _buildAppSection(),
_buildGeneralSection(), _buildGeneralSection(),
_buildMoreSection(),
]; ];
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 32),
itemBuilder: (_, index) { itemBuilder: (_, index) {
return Container( return Container(
alignment: Alignment.center, alignment: Alignment.center,

View File

@@ -1,140 +1,435 @@
// import 'dart:async'; import 'dart:async';
// import 'dart:io';
// import 'package:fl_clash/clash/core.dart';
// import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/clash/clash.dart';
// import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/common/common.dart';
// import 'package:fl_clash/state.dart'; import 'package:fl_clash/enum/enum.dart';
// import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/models/models.dart';
// import 'package:flutter/material.dart'; import 'package:fl_clash/plugins/app.dart';
// import 'package:fl_clash/widgets/widgets.dart';
// class ConnectionsFragment extends StatefulWidget { import 'package:flutter/material.dart';
// const ConnectionsFragment({super.key}); import 'package:provider/provider.dart';
//
// @override class ConnectionsFragment extends StatefulWidget {
// State<ConnectionsFragment> createState() => _ConnectionsFragmentState(); const ConnectionsFragment({super.key});
// }
// @override
// class _ConnectionsFragmentState extends State<ConnectionsFragment> { State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
// final connectionsNotifier = ValueNotifier<List<Connection>>([]); }
// Map<String, String?> idPackageNameMap = {};
// class _ConnectionsFragmentState extends State<ConnectionsFragment> {
// Timer? timer; final connectionsNotifier =
// ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
// @override final ScrollController _scrollController = ScrollController(
// void initState() { keepScrollOffset: false,
// super.initState(); );
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// _getConnections(); Timer? timer;
// if (timer != null) {
// timer?.cancel(); @override
// timer = null; void initState() {
// } super.initState();
// timer = Timer.periodic(const Duration(seconds: 3), (timer) { WidgetsBinding.instance.addPostFrameCallback((_) {
// if (mounted) { connectionsNotifier.value = connectionsNotifier.value
// _getConnections(); .copyWith(connections: clashCore.getConnections());
// } if (timer != null) {
// }); timer?.cancel();
// }); timer = null;
// } }
// timer = Timer.periodic(
// _getConnections() { const Duration(seconds: 1),
// connectionsNotifier.value = clashCore (timer) {
// .getConnections(); connectionsNotifier.value = connectionsNotifier.value
// } .copyWith(connections: clashCore.getConnections());
// },
// @override );
// void dispose() { });
// super.dispose(); }
// timer?.cancel();
// timer = null; _initActions() {
// } WidgetsBinding.instance.addPostFrameCallback(
// (_) {
// Future<ImageProvider?> _getPackageIconWithConnection( final commonScaffoldState =
// Connection connection) async { context.findAncestorStateOfType<CommonScaffoldState>();
// final uid = connection.metadata.uid; commonScaffoldState?.actions = [
// // if(globalState.packageNameMap[uid] == null){ IconButton(
// // globalState.packageNameMap[uid] = await app?.getPackageName(connection.metadata); onPressed: () {
// // } showSearch(
// final packageName = globalState.packageNameMap[uid]; context: context,
// if(packageName == null) return null; delegate: ConnectionsSearchDelegate(
// return await app?.getPackageIcon(packageName); state: connectionsNotifier.value,
// } ),
// );
// @override },
// Widget build(BuildContext context) { icon: const Icon(Icons.search),
// return ValueListenableBuilder<List<Connection>>( ),
// valueListenable: connectionsNotifier, const SizedBox(
// builder: (_, List<Connection> connections, __) { width: 8,
// if (connections.isEmpty) { )
// return const NullStatus( ];
// label: "未开启代理,或者没有连接数据", },
// ); );
// } }
// return ListView.separated(
// physics: const AlwaysScrollableScrollPhysics(), _addKeyword(String keyword) {
// itemBuilder: (_, index) { final isContains = connectionsNotifier.value.keywords.contains(keyword);
// final connection = connections[index]; if (isContains) return;
// return ListTile( final keywords = List<String>.from(connectionsNotifier.value.keywords)
// titleAlignment: ListTileTitleAlignment.top, ..add(keyword);
// leading: Container( connectionsNotifier.value = connectionsNotifier.value.copyWith(
// margin: const EdgeInsets.only(top: 4), keywords: keywords,
// width: 48, );
// height: 48, }
// child: FutureBuilder<ImageProvider?>(
// future: _getPackageIconWithConnection(connection), _deleteKeyword(String keyword) {
// builder: (_, snapshot) { final isContains = connectionsNotifier.value.keywords.contains(keyword);
// if (!snapshot.hasData && snapshot.data == null) { if (!isContains) return;
// return Container(); final keywords = List<String>.from(connectionsNotifier.value.keywords)
// } else { ..remove(keyword);
// return Image( connectionsNotifier.value = connectionsNotifier.value.copyWith(
// image: snapshot.data!, keywords: keywords,
// gaplessPlayback: true, );
// width: 48, }
// height: 48,
// ); _handleBlockConnection(String id) {
// } clashCore.closeConnections(id);
// }, connectionsNotifier.value = connectionsNotifier.value
// ), .copyWith(connections: clashCore.getConnections());
// ), }
// contentPadding:
// const EdgeInsets.symmetric(vertical: 12, horizontal: 16), @override
// title: Column( void dispose() {
// crossAxisAlignment: CrossAxisAlignment.start, super.dispose();
// children: [ timer?.cancel();
// Text(connection.metadata.host.isNotEmpty connectionsNotifier.dispose();
// ? connection.metadata.host _scrollController.dispose();
// : connection.metadata.destinationIP), timer = null;
// Padding( }
// padding: const EdgeInsets.only(
// top: 12, @override
// ), Widget build(BuildContext context) {
// child: Wrap( return Selector<AppState, bool?>(
// runSpacing: 8, selector: (_, appState) =>
// spacing: 8, appState.currentLabel == 'connections' ||
// children: [ appState.viewMode == ViewMode.mobile &&
// for (final chain in connection.chains) appState.currentLabel == "tools",
// CommonChip( builder: (_, isCurrent, child) {
// label: chain, if (isCurrent == null || isCurrent) {
// ), _initActions();
// ], }
// ), return child!;
// ), },
// ], child: ValueListenableBuilder<ConnectionsAndKeywords>(
// ), valueListenable: connectionsNotifier,
// trailing: IconButton( builder: (_, state, __) {
// icon: const Icon(Icons.block), var connections = state.filteredConnections;
// onPressed: () {}, if (connections.isEmpty) {
// ), return NullStatus(
// ); label: appLocalizations.nullConnectionsDesc,
// }, );
// separatorBuilder: (BuildContext context, int index) { }
// return const Divider( connections = connections.reversed.toList();
// height: 0, return Column(
// ); crossAxisAlignment: CrossAxisAlignment.start,
// }, children: [
// itemCount: connections.length, if (state.keywords.isNotEmpty)
// ); Padding(
// }, padding: const EdgeInsets.symmetric(
// ); horizontal: 16,
// } vertical: 16,
// } ),
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: _addKeyword,
onBlock: _handleBlockConnection,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
)
],
);
},
),
);
}
}
class ConnectionItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClick;
final Function(String)? onBlock;
const ConnectionItem({
super.key,
required this.connection,
this.onClick,
this.onBlock,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
return await app?.getPackageIcon(connection.metadata.process);
}
String _getRequestText(Metadata metadata) {
var text = "${metadata.network}:://";
final ips = [
metadata.host,
metadata.destinationIP,
].where((ip) => ip.isNotEmpty);
text += ips.join("/");
text += ":${metadata.destinationPort}";
return text;
}
String _getSourceText(Connection connection) {
final metadata = connection.metadata;
if (metadata.process.isEmpty) {
return connection.start.lastUpdateTimeDesc;
}
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
}
@override
Widget build(BuildContext context) {
return ListItem(
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: Platform.isAndroid
? Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
)
: null,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 12,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 12,
),
Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
const SizedBox(
height: 12,
),
],
),
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
if (onBlock == null) return;
onBlock!(connection.id);
},
),
);
}
}
class ConnectionsSearchDelegate extends SearchDelegate {
ValueNotifier<ConnectionsAndKeywords> connectionsNotifier;
ConnectionsSearchDelegate({
required ConnectionsAndKeywords state,
}) : connectionsNotifier = ValueNotifier<ConnectionsAndKeywords>(state);
get state => connectionsNotifier.value;
List<Connection> get _results {
final lowerQuery = query.toLowerCase().trim();
return connectionsNotifier.value.filteredConnections.where((request) {
final lowerNetwork = request.metadata.network.toLowerCase();
final lowerHost = request.metadata.host.toLowerCase();
final lowerDestinationIP = request.metadata.destinationIP.toLowerCase();
final lowerProcess = request.metadata.process.toLowerCase();
final lowerChains = request.chains.join("").toLowerCase();
return lowerNetwork.contains(lowerQuery) ||
lowerHost.contains(lowerQuery) ||
lowerDestinationIP.contains(lowerQuery) ||
lowerProcess.contains(lowerQuery) ||
lowerChains.contains(lowerQuery);
}).toList();
}
_addKeyword(String keyword) {
final isContains = connectionsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(connectionsNotifier.value.keywords)
..add(keyword);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = connectionsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(connectionsNotifier.value.keywords)
..remove(keyword);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
keywords: keywords,
);
}
_handleBlockConnection(String id) {
clashCore.closeConnections(id);
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
}
@override
List<Widget>? buildActions(BuildContext context) {
return [
IconButton(
onPressed: () {
if (query.isEmpty) {
close(context, null);
return;
}
query = '';
},
icon: const Icon(Icons.clear),
),
const SizedBox(
width: 8,
)
];
}
@override
Widget? buildLeading(BuildContext context) {
return IconButton(
onPressed: () {
close(context, null);
},
icon: const Icon(Icons.arrow_back),
);
}
@override
Widget buildResults(BuildContext context) {
return buildSuggestions(context);
}
@override
void dispose() {
connectionsNotifier.dispose();
super.dispose();
}
@override
Widget buildSuggestions(BuildContext context) {
return ValueListenableBuilder(
valueListenable: connectionsNotifier,
builder: (_, __, ___) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.keywords.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
itemBuilder: (_, index) {
final connection = _results[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: _addKeyword,
onBlock: _handleBlockConnection,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: _results.length,
),
)
],
);
},
);
}
}

View File

@@ -13,6 +13,7 @@ class CoreInfo extends StatelessWidget {
selector: (_, appState) => appState.versionInfo, selector: (_, appState) => appState.versionInfo,
builder: (_, versionInfo, __) { builder: (_, versionInfo, __) {
return CommonCard( return CommonCard(
onPressed: () {},
info: Info( info: Info(
label: appLocalizations.coreInfo, label: appLocalizations.coreInfo,
iconData: Icons.memory, iconData: Icons.memory,
@@ -31,7 +32,7 @@ class CoreInfo extends StatelessWidget {
style: context style: context
.textTheme .textTheme
.titleMedium .titleMedium
?.toSoftBold(), ?.toSoftBold,
), ),
), ),
const SizedBox( const SizedBox(
@@ -44,7 +45,7 @@ class CoreInfo extends StatelessWidget {
style: context style: context
.textTheme .textTheme
.titleLarge .titleLarge
?.toSoftBold(), ?.toSoftBold,
), ),
), ),
], ],

View File

@@ -1,11 +1,11 @@
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'network_detection.dart'; import 'network_detection.dart';
import 'core_info.dart';
import 'outbound_mode.dart'; import 'outbound_mode.dart';
import 'start_button.dart'; import 'start_button.dart';
import 'network_speed.dart'; import 'network_speed.dart';
@@ -56,7 +56,7 @@ class _DashboardFragmentState extends State<DashboardFragment> {
), ),
GridItem( GridItem(
crossAxisCellCount: isDesktop ? 4 : 6, crossAxisCellCount: isDesktop ? 4 : 6,
child: const CoreInfo(), child: const IntranetIp(),
), ),
], ],
); );

View File

@@ -0,0 +1,92 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
class IntranetIp extends StatefulWidget {
const IntranetIp({super.key});
@override
State<IntranetIp> createState() => _IntranetIpState();
}
class _IntranetIpState extends State<IntranetIp> {
final ipNotifier = ValueNotifier<String>("");
Future<String?> getLocalIpAddress() async {
List<NetworkInterface> interfaces = await NetworkInterface.list();
for (final interface in interfaces) {
for (final address in interface.addresses) {
if (!address.isLoopback) {
return address.address;
}
}
}
return null;
}
@override
void dispose() {
super.dispose();
ipNotifier.dispose();
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
ipNotifier.value = await getLocalIpAddress() ?? "";
});
}
@override
Widget build(BuildContext context) {
return CommonCard(
info: Info(
label: appLocalizations.intranetIp,
iconData: Icons.devices,
),
onPressed: (){
},
child: Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
height: globalState.appController.measure.titleLargeHeight + 24 - 1,
child: ValueListenableBuilder(
valueListenable: ipNotifier,
builder: (_, value, __) {
return FadeBox(
child: value.isNotEmpty
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
value,
style: context.textTheme.titleLarge?.toSoftBold.toMinus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
)
: const Padding(
padding: EdgeInsets.all(2),
child: AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
),
);
},
),
),
);
}
}

View File

@@ -19,6 +19,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
final timeoutNotifier = ValueNotifier<bool>(false); final timeoutNotifier = ValueNotifier<bool>(false);
bool? _preIsStart; bool? _preIsStart;
CancelToken? cancelToken; CancelToken? cancelToken;
Function? _checkIpDebounce;
_checkIp( _checkIp(
bool isInit, bool isInit,
@@ -44,6 +45,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
} }
_checkIpContainer(Widget child) { _checkIpContainer(Widget child) {
_checkIpDebounce = debounce(_checkIp);
return Selector2<AppState, Config, CheckIpSelectorState>( return Selector2<AppState, Config, CheckIpSelectorState>(
selector: (_, appState, config) { selector: (_, appState, config) {
return CheckIpSelectorState( return CheckIpSelectorState(
@@ -53,13 +55,22 @@ class _NetworkDetectionState extends State<NetworkDetection> {
); );
}, },
builder: (_, state, __) { builder: (_, state, __) {
_checkIp(state.isInit, state.isStart); if (_checkIpDebounce != null) {
_checkIpDebounce!([state.isInit, state.isStart]);
}
return child; return child;
}, },
child: child, child: child,
); );
} }
@override
void dispose() {
super.dispose();
ipInfoNotifier.dispose();
timeoutNotifier.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _checkIpContainer( return _checkIpContainer(
@@ -67,6 +78,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
valueListenable: ipInfoNotifier, valueListenable: ipInfoNotifier,
builder: (_, ipInfo, __) { builder: (_, ipInfo, __) {
return CommonCard( return CommonCard(
onPressed: () {},
child: Column( child: Column(
children: [ children: [
Flexible( Flexible(
@@ -123,8 +135,9 @@ class _NetworkDetectionState extends State<NetworkDetection> {
), ),
), ),
Container( Container(
height: height: globalState.appController.measure.titleLargeHeight +
globalState.appController.measure.titleLargeHeight + 24, 24 -
1,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0), padding: const EdgeInsets.all(16).copyWith(top: 0),
child: FadeBox( child: FadeBox(
@@ -139,7 +152,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
text: Text( text: Text(
ipInfo.ip, ipInfo.ip,
style: context.textTheme.titleLarge style: context.textTheme.titleLarge
?.toSoftBold(), ?.toSoftBold.toMinus,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -153,9 +166,10 @@ class _NetworkDetectionState extends State<NetworkDetection> {
if (timeout) { if (timeout) {
return Text( return Text(
"timeout", "timeout",
style: context.textTheme.titleMedium style: context.textTheme.titleLarge
?.copyWith(color: Colors.red) ?.copyWith(color: Colors.red)
.toSoftBold(), .toSoftBold
.toMinus,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
); );

View File

@@ -21,26 +21,17 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
.asMap() .asMap()
.map( .map(
(index, e) => MapEntry( (index, e) => MapEntry(
index, index,
Point( Point(
(index + initPoints.length).toDouble(), (index + initPoints.length).toDouble(),
e.speed.toDouble(), e.speed.toDouble(),
), ),
), ),
) )
.values .values
.toList(); .toList();
var pointsRaw = [...initPoints, ...trafficPoints];
List<Point> points;
if (pointsRaw.length > 60) {
points = pointsRaw
.getRange(pointsRaw.length - 61, pointsRaw.length - 1)
.toList();
} else {
points = pointsRaw;
}
return points; return [...initPoints, ...trafficPoints];
} }
Traffic _getLastTraffic(List<Traffic> traffics) { Traffic _getLastTraffic(List<Traffic> traffics) {
@@ -53,12 +44,11 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
required IconData iconData, required IconData iconData,
required TrafficValue value, required TrafficValue value,
}) { }) {
final showValue = value.showValue; final showValue = value.showValue;
final showUnit = "${value.showUnit}/s"; final showUnit = "${value.showUnit}/s";
final titleLargeSoftBold = final titleLargeSoftBold =
Theme.of(context).textTheme.titleLarge?.toSoftBold(); Theme.of(context).textTheme.titleLarge?.toSoftBold;
final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight(); final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight;
final valueText = Text( final valueText = Text(
showValue, showValue,
style: titleLargeSoftBold, style: titleLargeSoftBold,
@@ -85,7 +75,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
Flexible( Flexible(
child: Text( child: Text(
label, label,
style: Theme.of(context).textTheme.titleSmall?.toSoftBold(), style: Theme.of(context).textTheme.titleSmall?.toSoftBold,
), ),
), ),
], ],
@@ -121,7 +111,8 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CommonCard( return CommonCard(
info: Info( onPressed: () {},
info: Info(
label: appLocalizations.networkSpeed, label: appLocalizations.networkSpeed,
iconData: Icons.speed, iconData: Icons.speed,
), ),
@@ -172,4 +163,4 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
), ),
); );
} }
} }

View File

@@ -33,6 +33,7 @@ class OutboundMode extends StatelessWidget {
selector: (_, clashConfig) => clashConfig.mode, selector: (_, clashConfig) => clashConfig.mode,
builder: (_, mode, __) { builder: (_, mode, __) {
return CommonCard( return CommonCard(
onPressed: () {},
info: Info( info: Info(
label: appLocalizations.outboundMode, label: appLocalizations.outboundMode,
iconData: Icons.call_split, iconData: Icons.call_split,
@@ -67,7 +68,7 @@ class OutboundMode extends StatelessWidget {
.of(context) .of(context)
.textTheme .textTheme
.titleMedium .titleMedium
?.toSoftBold(), ?.toSoftBold,
), ),
), ),
], ],

View File

@@ -13,19 +13,17 @@ class StartButton extends StatefulWidget {
class _StartButtonState extends State<StartButton> class _StartButtonState extends State<StartButton>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
bool isStart = false;
bool isInit = false;
late AnimationController _controller; late AnimationController _controller;
bool isStart = false;
@override @override
void initState() { void initState() {
isStart = globalState.appController.appState.isStart; super.initState();
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
value: isStart ? 1 : 0, value: 0,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
); );
super.initState();
} }
@override @override
@@ -35,9 +33,12 @@ class _StartButtonState extends State<StartButton>
} }
handleSwitchStart() { handleSwitchStart() {
isStart = !isStart; final appController = globalState.appController;
updateController(); if (isStart == appController.appState.isStart) {
updateSystemProxy(); isStart = !isStart;
updateController();
appController.updateSystemProxy(isStart);
}
} }
updateController() { updateController() {
@@ -48,11 +49,18 @@ class _StartButtonState extends State<StartButton>
} }
} }
updateSystemProxy() { Widget _updateControllerContainer(Widget child) {
WidgetsBinding.instance.addPostFrameCallback((_) async { return Selector<AppState, bool>(
final appController = globalState.appController; selector: (_, appState) => appState.isStart,
await appController.updateSystemProxy(isStart); builder: (_, isStart, child) {
}); if(isStart != this.isStart){
this.isStart = isStart;
updateController();
}
return child!;
},
child: child,
);
} }
@override @override
@@ -72,8 +80,7 @@ class _StartButtonState extends State<StartButton>
other.getTimeDifference( other.getTimeDifference(
DateTime.now(), DateTime.now(),
), ),
style: style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
Theme.of(context).textTheme.titleMedium?.toSoftBold(),
), ),
) )
.width + .width +
@@ -119,24 +126,14 @@ class _StartButtonState extends State<StartButton>
child: child, child: child,
); );
}, },
child: Selector<AppState, bool>( child: _updateControllerContainer(
selector: (_, appState) => appState.runTime != null, Selector<AppState, int?>(
builder: (_, isRun, child) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (isStart != isRun) {
isStart = isRun;
updateController();
}
});
return child!;
},
child: Selector<AppState, int?>(
selector: (_, appState) => appState.runTime, selector: (_, appState) => appState.runTime,
builder: (_, int? value, __) { builder: (_, int? value, __) {
final text = other.getTimeText(value); final text = other.getTimeText(value);
return Text( return Text(
text, text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold(), style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
); );
}, },
), ),

View File

@@ -42,7 +42,7 @@ class TrafficUsage extends StatelessWidget {
), ),
Text( Text(
trafficValue.showUnit, trafficValue.showUnit,
style: context.textTheme.labelMedium?.toLight(), style: context.textTheme.labelMedium?.toLight,
), ),
], ],
); );
@@ -51,25 +51,16 @@ class TrafficUsage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CommonCard( return CommonCard(
onPressed: () {},
info: Info( info: Info(
label: appLocalizations.trafficUsage, label: appLocalizations.trafficUsage,
iconData: Icons.data_saver_off, iconData: Icons.data_saver_off,
), ),
child: Selector<AppState, List<Traffic>>( child: Selector<AppState, Traffic>(
selector: (_, appState) => appState.traffics, selector: (_, appState) => appState.totalTraffic,
builder: (_, traffics, __) { builder: (_, totalTraffic, __) {
final trafficTotal = traffics.isNotEmpty final upTotalTrafficValue = totalTraffic.up;
? traffics.reduce( final downTotalTrafficValue = totalTraffic.down;
(value, element) {
return Traffic(
up: element.up.value + value.up.value,
down: element.down.value + value.down.value,
);
},
)
: Traffic();
final upTrafficValue = trafficTotal.up;
final downTrafficValue = trafficTotal.down;
return Padding( return Padding(
padding: const EdgeInsets.all(16).copyWith(top: 0), padding: const EdgeInsets.all(16).copyWith(top: 0),
child: Column( child: Column(
@@ -80,7 +71,7 @@ class TrafficUsage extends StatelessWidget {
child: getTrafficDataItem( child: getTrafficDataItem(
context, context,
Icons.arrow_upward, Icons.arrow_upward,
upTrafficValue, upTotalTrafficValue,
), ),
), ),
const SizedBox( const SizedBox(
@@ -91,7 +82,7 @@ class TrafficUsage extends StatelessWidget {
child: getTrafficDataItem( child: getTrafficDataItem(
context, context,
Icons.arrow_downward, Icons.arrow_downward,
downTrafficValue, downTotalTrafficValue,
), ),
), ),
], ],

View File

@@ -17,7 +17,12 @@ class LogsFragment extends StatefulWidget {
} }
class _LogsFragmentState extends State<LogsFragment> { class _LogsFragmentState extends State<LogsFragment> {
final logsNotifier = ValueNotifier<List<Log>>([]); final logsNotifier = ValueNotifier<LogsAndKeywords>(const LogsAndKeywords());
final scrollController = ScrollController(
keepScrollOffset: false,
);
List<GlobalObjectKey<_LogItemState>> keys = [];
Timer? timer; Timer? timer;
@override @override
@@ -25,18 +30,18 @@ class _LogsFragmentState extends State<LogsFragment> {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState; final appState = globalState.appController.appState;
logsNotifier.value = List<Log>.from(appState.logs); logsNotifier.value = logsNotifier.value.copyWith(logs: appState.logs);
if (timer != null) { if (timer != null) {
timer?.cancel(); timer?.cancel();
timer = null; timer = null;
} }
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) { timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final logs = List<Log>.from(appState.logs); final logs = appState.logs;
if (!const ListEquality<Log>().equals( if (!const ListEquality<Log>().equals(
logsNotifier.value, logsNotifier.value.logs,
logs, logs,
)) { )) {
logsNotifier.value = logs; logsNotifier.value = logsNotifier.value.copyWith(logs: logs);
} }
}); });
}); });
@@ -46,6 +51,8 @@ class _LogsFragmentState extends State<LogsFragment> {
void dispose() { void dispose() {
super.dispose(); super.dispose();
timer?.cancel(); timer?.cancel();
logsNotifier.dispose();
scrollController.dispose();
timer = null; timer = null;
} }
@@ -59,7 +66,7 @@ class _LogsFragmentState extends State<LogsFragment> {
showSearch( showSearch(
context: context, context: context,
delegate: LogsSearchDelegate( delegate: LogsSearchDelegate(
logs: logsNotifier.value.reversed.toList(), logs: logsNotifier.value,
), ),
); );
}, },
@@ -72,32 +79,23 @@ class _LogsFragmentState extends State<LogsFragment> {
}); });
} }
_buildList() { _addKeyword(String keyword) {
return ValueListenableBuilder<List<Log>>( final isContains = logsNotifier.value.keywords.contains(keyword);
valueListenable: logsNotifier, if (isContains) return;
builder: (_, List<Log> logs, __) { final keywords = List<String>.from(logsNotifier.value.keywords)
if (logs.isEmpty) { ..add(keyword);
return NullStatus( logsNotifier.value = logsNotifier.value.copyWith(
label: appLocalizations.nullLogsDesc, keywords: keywords,
); );
} }
logs = logs.reversed.toList();
return ListView.separated( _deleteKeyword(String keyword) {
physics: const AlwaysScrollableScrollPhysics(), final isContains = logsNotifier.value.keywords.contains(keyword);
itemCount: logs.length, if (!isContains) return;
itemBuilder: (BuildContext context, int index) { final keywords = List<String>.from(logsNotifier.value.keywords)
final log = logs[index]; ..remove(keyword);
return LogItem( logsNotifier.value = logsNotifier.value.copyWith(
log: log, keywords: keywords,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
);
},
); );
} }
@@ -114,21 +112,88 @@ class _LogsFragmentState extends State<LogsFragment> {
} }
return child!; return child!;
}, },
child: _buildList(), child: ValueListenableBuilder<LogsAndKeywords>(
valueListenable: logsNotifier,
builder: (_, state, __) {
var logs = state.filteredLogs;
if (logs.isEmpty) {
return NullStatus(
label: appLocalizations.nullLogsDesc,
);
}
logs = logs.reversed.toList();
keys = logs
.map((log) => GlobalObjectKey<_LogItemState>(log.dateTime))
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.keywords.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
controller: scrollController,
itemBuilder: (_, index) {
final log = logs[index];
return LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: _addKeyword,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: logs.length,
),
)
],
);
},
),
); );
} }
} }
class LogsSearchDelegate extends SearchDelegate { class LogsSearchDelegate extends SearchDelegate {
List<Log> logs = []; ValueNotifier<LogsAndKeywords> logsNotifier;
LogsSearchDelegate({ LogsSearchDelegate({
required this.logs, required LogsAndKeywords logs,
}); }) : logsNotifier = ValueNotifier(logs);
@override
void dispose() {
super.dispose();
logsNotifier.dispose();
}
get state => logsNotifier.value;
List<Log> get _results { List<Log> get _results {
final lowQuery = query.toLowerCase(); final lowQuery = query.toLowerCase();
return logs return logsNotifier.value.filteredLogs
.where( .where(
(log) => (log) =>
(log.payload?.toLowerCase().contains(lowQuery) ?? false) || (log.payload?.toLowerCase().contains(lowQuery) ?? false) ||
@@ -171,37 +236,98 @@ class LogsSearchDelegate extends SearchDelegate {
return buildSuggestions(context); return buildSuggestions(context);
} }
_addKeyword(String keyword) {
final isContains = logsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(logsNotifier.value.keywords)..add(keyword);
logsNotifier.value = logsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = logsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(logsNotifier.value.keywords)..remove(keyword);
logsNotifier.value = logsNotifier.value.copyWith(
keywords: keywords,
);
}
@override @override
Widget buildSuggestions(BuildContext context) { Widget buildSuggestions(BuildContext context) {
return ListView.separated( return ValueListenableBuilder(
physics: const AlwaysScrollableScrollPhysics(), valueListenable: logsNotifier,
itemCount: _results.length, builder: (_, __, ___) {
itemBuilder: (BuildContext context, int index) { return Column(
final log = _results[index]; crossAxisAlignment: CrossAxisAlignment.start,
return LogItem( children: [
key: ValueKey(log.dateTime), if (state.keywords.isNotEmpty)
log: log, Padding(
); padding: const EdgeInsets.symmetric(
}, horizontal: 16,
separatorBuilder: (BuildContext context, int index) { vertical: 16,
return const Divider( ),
height: 0, child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
itemBuilder: (_, index) {
final log = _results[index];
return LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: (value) {
_addKeyword(value);
},
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: _results.length,
),
)
],
); );
}, },
); );
} }
} }
class LogItem extends StatelessWidget { class LogItem extends StatefulWidget {
final Log log; final Log log;
final Function(String)? onClick;
const LogItem({ const LogItem({
super.key, super.key,
required this.log, required this.log,
this.onClick,
}); });
@override
State<LogItem> createState() => _LogItemState();
}
class _LogItemState extends State<LogItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final log = widget.log;
return ListTile( return ListTile(
title: SelectableText(log.payload ?? ''), title: SelectableText(log.payload ?? ''),
subtitle: Column( subtitle: Column(
@@ -223,6 +349,10 @@ class LogItem extends StatelessWidget {
vertical: 8, vertical: 8,
), ),
child: CommonChip( child: CommonChip(
onPressed: () {
if (widget.onClick == null) return;
widget.onClick!(log.logLevel.name);
},
label: log.logLevel.name, label: log.logLevel.name,
), ),
), ),

View File

@@ -91,6 +91,7 @@ class _URLFormDialogState extends State<URLFormDialog> {
runSpacing: 16, runSpacing: 16,
children: [ children: [
TextField( TextField(
maxLines: null,
controller: urlController, controller: urlController,
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),

View File

@@ -41,19 +41,26 @@ class _EditProfileState extends State<EditProfile> {
_handleConfirm() { _handleConfirm() {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
final config = widget.context.read<Config>(); final config = widget.context.read<Config>();
final hasUpdate = urlController.text.isNotEmpty && widget.profile.url != urlController.text; final profile = widget.profile.copyWith(
widget.profile.url = urlController.text; url: urlController.text,
widget.profile.label = labelController.text; label: labelController.text,
widget.profile.autoUpdate = autoUpdate; autoUpdate: autoUpdate,
widget.profile.autoUpdateDuration = autoUpdateDuration: Duration(
Duration(minutes: int.parse(autoUpdateDurationController.text)); minutes: int.parse(
config.setProfile(widget.profile); autoUpdateDurationController.text,
),
),
);
final hasUpdate = widget.profile.url != profile.url;
config.setProfile(profile);
if (hasUpdate) { if (hasUpdate) {
widget.context.findAncestorStateOfType<CommonScaffoldState>()?.loadingRun( globalState.homeScaffoldKey.currentState?.loadingRun(
() => globalState.appController.updateProfile( () async {
widget.profile.id, if (hasUpdate) {
), await globalState.appController.updateProfile(profile);
); }
},
);
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
@@ -83,12 +90,11 @@ class _EditProfileState extends State<EditProfile> {
}, },
), ),
), ),
if (widget.profile.type == ProfileType.url)...[ if (widget.profile.type == ProfileType.url) ...[
ListItem( ListItem(
title: TextFormField( title: TextFormField(
controller: urlController, controller: urlController,
minLines: 1, maxLines: null,
maxLines: 2,
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: appLocalizations.url, labelText: appLocalizations.url,

View File

@@ -1,5 +1,6 @@
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart'; import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/fragments/profiles/view_profile.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -15,6 +16,7 @@ enum ProfileActions {
edit, edit,
update, update,
delete, delete,
view,
} }
class ProfilesFragment extends StatefulWidget { class ProfilesFragment extends StatefulWidget {
@@ -27,6 +29,8 @@ class ProfilesFragment extends StatefulWidget {
class _ProfilesFragmentState extends State<ProfilesFragment> { class _ProfilesFragmentState extends State<ProfilesFragment> {
final hasPadding = ValueNotifier<bool>(false); final hasPadding = ValueNotifier<bool>(false);
List<GlobalObjectKey<_ProfileItemState>> profileItemKeys = [];
_handleShowAddExtendPage() { _handleShowAddExtendPage() {
showExtendPage( showExtendPage(
globalState.navigatorKey.currentState!.context, globalState.navigatorKey.currentState!.context,
@@ -48,19 +52,22 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
} }
} }
_updateProfiles() async {
final updateProfiles = profileItemKeys.map<Future>(
(key) async => await key.currentState?.updateProfile(false));
final result = await Future.wait(updateProfiles);
}
_initScaffoldState() { _initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(_) { (_) {
final commonScaffoldState = final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>(); context.findAncestorStateOfType<CommonScaffoldState>();
if (!context.mounted) return;
commonScaffoldState?.actions = [ commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () {
commonScaffoldState.loadingRun<void>( _updateProfiles();
() async {
await globalState.appController.updateProfiles();
},
);
}, },
icon: const Icon(Icons.sync), icon: const Icon(Icons.sync),
), ),
@@ -79,6 +86,12 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
); );
} }
@override
void dispose() {
super.dispose();
hasPadding.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<AppState, bool>( return Selector<AppState, bool>(
@@ -101,6 +114,9 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
label: appLocalizations.nullProfileDesc, label: appLocalizations.nullProfileDesc,
); );
} }
profileItemKeys = state.profiles
.map((profile) => GlobalObjectKey<_ProfileItemState>(profile.id))
.toList();
final columns = _getColumns(state.viewMode); final columns = _getColumns(state.viewMode);
final isMobile = state.viewMode == ViewMode.mobile; final isMobile = state.viewMode == ViewMode.mobile;
return Align( return Align(
@@ -132,10 +148,11 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
crossAxisSpacing: 16, crossAxisSpacing: 16,
crossAxisCount: columns, crossAxisCount: columns,
children: [ children: [
for (final profile in state.profiles) for (int i = 0; i < state.profiles.length; i++)
GridItem( GridItem(
child: ProfileItem( child: ProfileItem(
profile: profile, key: profileItemKeys[i],
profile: state.profiles[i],
groupValue: state.currentProfileId, groupValue: state.currentProfileId,
onChanged: onChanged:
globalState.appController.changeProfile, globalState.appController.changeProfile,
@@ -173,42 +190,51 @@ class ProfileItem extends StatefulWidget {
class _ProfileItemState extends State<ProfileItem> { class _ProfileItemState extends State<ProfileItem> {
final isUpdating = ValueNotifier<bool>(false); final isUpdating = ValueNotifier<bool>(false);
_handleDeleteProfile(String id) async { _handleDeleteProfile() async {
globalState.appController.deleteProfile(id); globalState.appController.deleteProfile(widget.profile.id);
} }
_handleUpdateProfile(String id) async { _handleUpdateProfile() async {
await globalState.safeRun<void>(updateProfile);
}
Future updateProfile([isSingle = true]) async {
isUpdating.value = true; isUpdating.value = true;
await globalState.safeRun<void>(() async { try {
await globalState.appController.updateProfile(id); await globalState.appController.updateProfile(widget.profile);
}); } catch (e) {
isUpdating.value = false;
if (!isSingle) {
return e.toString();
}
}
isUpdating.value = false; isUpdating.value = false;
return null;
} }
_handleShowEditExtendPage( _handleShowEditExtendPage() {
Profile profile,
) {
showExtendPage( showExtendPage(
context, context,
body: EditProfile( body: EditProfile(
profile: profile.copyWith(), profile: widget.profile,
context: context, context: context,
), ),
title: "${appLocalizations.edit}${appLocalizations.profile}", title: "${appLocalizations.edit}${appLocalizations.profile}",
); );
} }
_handleViewProfile() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ViewProfile(
profile: widget.profile,
),
),
);
}
_buildTitle(Profile profile) { _buildTitle(Profile profile) {
final textTheme = context.textTheme; final textTheme = context.textTheme;
final userInfo = profile.userInfo ?? UserInfo();
final use = userInfo.upload + userInfo.download;
final total = userInfo.total;
final useShow = TrafficValue(value: use).show;
final totalShow = TrafficValue(value: total).show;
final progress = total == 0 ? 0.0 : use / total;
final expireShow = userInfo.expire == 0
? "长期有效"
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000).show;
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(vertical: 4),
child: Column( child: Column(
@@ -227,53 +253,141 @@ class _ProfileItemState extends State<ProfileItem> {
), ),
Text( Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '', profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',
style: textTheme.labelMedium?.toLight(), style: textTheme.labelMedium?.toLight,
), ),
], ],
), ),
Column( Builder(builder: (context) {
mainAxisSize: MainAxisSize.min, final userInfo = profile.userInfo ?? const UserInfo();
crossAxisAlignment: CrossAxisAlignment.start, final use = userInfo.upload + userInfo.download;
mainAxisAlignment: MainAxisAlignment.center, final total = userInfo.total;
children: [ final useShow = TrafficValue(value: use).show;
Container( final totalShow = TrafficValue(value: total).show;
margin: const EdgeInsets.symmetric( final progress = total == 0 ? 0.0 : use / total;
vertical: 8, final expireShow = userInfo.expire == 0
? appLocalizations.infiniteTime
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000)
.show;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: LinearProgressIndicator(
minHeight: 6,
value: progress,
),
), ),
child: LinearProgressIndicator( Text(
minHeight: 6, "$useShow / $totalShow",
value: progress, style: textTheme.labelMedium?.toLight,
), ),
), const SizedBox(
Text( height: 2,
"$useShow / $totalShow", ),
style: textTheme.labelMedium?.toLight(), Row(
), children: [
const SizedBox( Text(
height: 2, appLocalizations.expirationTime,
), style: textTheme.labelMedium?.toLighter,
Row( ),
children: [ const SizedBox(
Text( width: 4,
"到期时间:", ),
style: textTheme.labelMedium?.toLighter(), Text(
), expireShow,
const SizedBox( style: textTheme.labelMedium?.toLighter,
width: 4, ),
), ],
Text( )
expireShow, ],
style: textTheme.labelMedium?.toLighter(), );
), // final child = switch (userInfo != null) {
], // true => () {
) // final use = userInfo!.upload + userInfo.download;
], // final total = userInfo.total;
), // final useShow = TrafficValue(value: use).show;
// final totalShow = TrafficValue(value: total).show;
// final progress = total == 0 ? 0.0 : use / total;
// final expireShow = userInfo.expire == 0
// ? appLocalizations.infiniteTime
// : DateTime.fromMillisecondsSinceEpoch(
// userInfo.expire * 1000)
// .show;
// return Column(
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Container(
// margin: const EdgeInsets.symmetric(
// vertical: 8,
// ),
// child: LinearProgressIndicator(
// minHeight: 6,
// value: progress,
// ),
// ),
// Text(
// "$useShow / $totalShow",
// style: textTheme.labelMedium?.toLight(),
// ),
// const SizedBox(
// height: 2,
// ),
// Row(
// children: [
// Text(
// appLocalizations.expirationTime,
// style: textTheme.labelMedium?.toLighter(),
// ),
// const SizedBox(
// width: 4,
// ),
// Text(
// expireShow,
// style: textTheme.labelMedium?.toLighter(),
// ),
// ],
// )
// ],
// );
// }(),
// false => Column(
// children: [
// Padding(
// padding: const EdgeInsets.only(top: 8),
// child: CommonChip(
// onPressed: _handleViewProfile,
// avatar: const Icon(Icons.remove_red_eye),
// label: appLocalizations.view,
// ),
// ),
// ],
// ),
// };
// final measure = globalState.appController.measure;
// final height = 6 + 8 * 2 + 2 + measure.labelMediumHeight * 2;
// return SizedBox(
// height: height,
// child: child,
// );
}),
], ],
), ),
); );
} }
@override
void dispose() {
isUpdating.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final profile = widget.profile; final profile = widget.profile;
@@ -298,40 +412,63 @@ class _ProfileItemState extends State<ProfileItem> {
onChanged: onChanged, onChanged: onChanged,
), ),
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
trailing: CommonPopupMenu<ProfileActions>( trailing: SizedBox(
items: [ height: 48,
CommonPopupMenuItem( width: 48,
action: ProfileActions.edit, child: ValueListenableBuilder(
label: appLocalizations.edit, valueListenable: isUpdating,
iconData: Icons.edit, builder: (_, isUpdating, ___) {
), return FadeBox(
if (profile.type == ProfileType.url) child: isUpdating
CommonPopupMenuItem( ? const Padding(
action: ProfileActions.update, padding: EdgeInsets.all(8),
label: appLocalizations.update, child: CircularProgressIndicator(),
iconData: Icons.sync, )
), : CommonPopupMenu<ProfileActions>(
CommonPopupMenuItem( items: [
action: ProfileActions.delete, CommonPopupMenuItem(
label: appLocalizations.delete, action: ProfileActions.edit,
iconData: Icons.delete, label: appLocalizations.edit,
), iconData: Icons.edit,
], ),
onSelected: (ProfileActions? action) async { if (profile.type == ProfileType.url)
switch (action) { CommonPopupMenuItem(
case ProfileActions.edit: action: ProfileActions.update,
_handleShowEditExtendPage(profile); label: appLocalizations.update,
break; iconData: Icons.sync,
case ProfileActions.delete: ),
_handleDeleteProfile(profile.id); CommonPopupMenuItem(
break; action: ProfileActions.delete,
case ProfileActions.update: label: appLocalizations.delete,
_handleUpdateProfile(profile.id); iconData: Icons.delete,
break; ),
case null: CommonPopupMenuItem(
break; action: ProfileActions.view,
} label: appLocalizations.view,
}, iconData: Icons.visibility,
),
],
onSelected: (ProfileActions? action) async {
switch (action) {
case ProfileActions.edit:
_handleShowEditExtendPage();
break;
case ProfileActions.delete:
_handleDeleteProfile();
break;
case ProfileActions.update:
_handleUpdateProfile();
break;
case ProfileActions.view:
_handleViewProfile();
break;
case null:
break;
}
},
));
},
),
), ),
title: _buildTitle(profile), title: _buildTitle(profile),
tileTitleAlignment: ListTileTitleAlignment.titleHeight, tileTitleAlignment: ListTileTitleAlignment.titleHeight,

View File

@@ -0,0 +1,207 @@
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/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:re_editor/re_editor.dart';
import 'package:re_highlight/languages/yaml.dart';
import 'package:re_highlight/styles/intellij-light.dart';
class ViewProfile extends StatefulWidget {
final Profile profile;
const ViewProfile({
super.key,
required this.profile,
});
@override
State<ViewProfile> createState() => _ViewProfileState();
}
class _ViewProfileState extends State<ViewProfile> {
bool readOnly = true;
CodeLineEditingController? controller;
final contentNotifier = ValueNotifier<String>("");
final key = GlobalKey<CommonScaffoldState>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
final profilePath = await appPath.getProfilePath(widget.profile.id);
if (profilePath == null) {
return;
}
final file = File(profilePath);
final text = await file.readAsString();
contentNotifier.value = text;
});
}
@override
void dispose() {
super.dispose();
contentNotifier.dispose();
controller?.dispose();
}
Profile get profile => widget.profile;
_handleChangeReadOnly() async {
if (readOnly == true) {
setState(() {
readOnly = false;
});
} else {
final text = controller?.text;
if (text == null || text == contentNotifier.value) {
setState(() {
readOnly = true;
});
return;
}
contentNotifier.value = text;
final newProfile = await key.currentState?.loadingRun<Profile>(() async {
return await profile.saveFileWithString(text);
});
if (newProfile == null) return;
globalState.appController.config.setProfile(newProfile);
setState(() {
readOnly = true;
});
}
}
@override
Widget build(BuildContext context) {
return CommonScaffold(
key: key,
actions: [
IconButton(
onPressed: controller?.undo,
icon: const Icon(Icons.undo),
),
IconButton(
onPressed: controller?.redo,
icon: const Icon(Icons.redo),
),
if (!widget.profile.realAutoUpdate)
IconButton(
onPressed: _handleChangeReadOnly,
icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
),
const SizedBox(
width: 8,
)
],
body: ValueListenableBuilder(
valueListenable: contentNotifier,
builder: (_, value, __) {
if (value.isEmpty) return Container();
controller = CodeLineEditingController.fromText(value);
return CodeEditor(
autofocus: false,
readOnly: readOnly,
scrollbarBuilder: (context, child, details) {
return Scrollbar(
controller: details.controller,
thickness: 8,
radius: const Radius.circular(2),
interactive: true,
child: child,
);
},
showCursorWhenReadOnly: false,
controller: controller,
toolbarController:
!readOnly ? const ContextMenuControllerImpl() : null,
shortcutsActivatorsBuilder:
const DefaultCodeShortcutsActivatorsBuilder(),
indicatorBuilder:
(context, editingController, chunkController, notifier) {
return Row(
children: [
DefaultCodeLineNumber(
controller: editingController,
notifier: notifier,
),
DefaultCodeChunkIndicator(
width: 20,
controller: chunkController,
notifier: notifier,
)
],
);
},
style: CodeEditorStyle(
fontSize: 14,
codeTheme: CodeHighlightTheme(
languages: {
'yaml': CodeHighlightThemeMode(
mode: langYaml,
)
},
theme: intellijLightTheme,
),
),
);
},
),
title: widget.profile.label ?? widget.profile.id,
);
}
}
class ContextMenuItemWidget extends PopupMenuItem<void> {
ContextMenuItemWidget({
super.key,
required String text,
required VoidCallback super.onTap,
}) : super(child: Text(text));
}
class ContextMenuControllerImpl implements SelectionToolbarController {
const ContextMenuControllerImpl();
@override
void hide(BuildContext context) {}
@override
void show({
required BuildContext context,
required CodeLineEditingController controller,
required TextSelectionToolbarAnchors anchors,
Rect? renderRect,
required LayerLink layerLink,
required ValueNotifier<bool> visibility,
}) {
if (controller.selectedText.isEmpty) {
return;
}
showMenu(
context: context,
position: RelativeRect.fromSize(
(anchors.secondaryAnchor ?? anchors.primaryAnchor) &
const Size(150, double.infinity),
MediaQuery.of(context).size,
),
items: [
ContextMenuItemWidget(
text: appLocalizations.cut,
onTap: controller.cut,
),
ContextMenuItemWidget(
text: appLocalizations.copy,
onTap: controller.copy,
),
ContextMenuItemWidget(
text: appLocalizations.paste,
onTap: controller.paste,
),
],
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,21 +3,24 @@ import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.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/models/models.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class RequestFragment extends StatefulWidget { class RequestsFragment extends StatefulWidget {
const RequestFragment({super.key}); const RequestsFragment({super.key});
@override @override
State<RequestFragment> createState() => _RequestFragmentState(); State<RequestsFragment> createState() => _RequestsFragmentState();
} }
class _RequestFragmentState extends State<RequestFragment> { class _RequestsFragmentState extends State<RequestsFragment> {
final requestsNotifier = ValueNotifier<List<Connection>>([]); final requestsNotifier =
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
final ScrollController _scrollController = ScrollController( final ScrollController _scrollController = ScrollController(
keepScrollOffset: false, keepScrollOffset: false,
); );
@@ -29,23 +32,70 @@ class _RequestFragmentState extends State<RequestFragment> {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState; final appState = globalState.appController.appState;
requestsNotifier.value = List<Connection>.from(appState.requests); requestsNotifier.value =
requestsNotifier.value.copyWith(connections: appState.requests);
if (timer != null) { if (timer != null) {
timer?.cancel(); timer?.cancel();
timer = null; timer = null;
} }
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) { timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final requests = List<Connection>.from(appState.requests); final requests = appState.requests;
if (!const ListEquality<Connection>().equals( if (!const ListEquality<Connection>().equals(
requestsNotifier.value, requestsNotifier.value.connections,
requests, requests,
)) { )) {
requestsNotifier.value = requests; requestsNotifier.value =
requestsNotifier.value.copyWith(connections: requests);
} }
}); });
}); });
} }
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
showSearch(
context: context,
delegate: RequestsSearchDelegate(
state: requestsNotifier.value,
),
);
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
)
];
},
);
}
_addKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..add(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..remove(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
@@ -56,42 +106,86 @@ class _RequestFragmentState extends State<RequestFragment> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ValueListenableBuilder<List<Connection>>( return Selector<AppState, bool?>(
valueListenable: requestsNotifier, selector: (_, appState) =>
builder: (_, List<Connection> connections, __) { appState.currentLabel == 'requests' ||
if (connections.isEmpty) { appState.viewMode == ViewMode.mobile &&
return NullStatus( appState.currentLabel == "tools",
label: appLocalizations.nullRequestsDesc, builder: (_, isCurrent, child) {
); if (isCurrent == null || isCurrent) {
_initActions();
} }
connections = connections.reversed.toList(); return child!;
return ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return RequestItem(
key: Key(connection.id),
connection: connection,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
);
}, },
child: ValueListenableBuilder<ConnectionsAndKeywords>(
valueListenable: requestsNotifier,
builder: (_, state, __) {
var connections = state.filteredConnections;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
connections = connections.reversed.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.keywords.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return RequestItem(
key: Key(connection.id),
connection: connection,
onClick: _addKeyword,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
)
],
);
},
),
); );
} }
} }
class RequestItem extends StatelessWidget { class RequestItem extends StatelessWidget {
final Connection connection; final Connection connection;
final Function(String)? onClick;
const RequestItem({ const RequestItem({
super.key, super.key,
required this.connection, required this.connection,
this.onClick,
}); });
Future<ImageProvider?> _getPackageIcon(Connection connection) async { Future<ImageProvider?> _getPackageIcon(Connection connection) async {
@@ -119,8 +213,8 @@ class RequestItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListItem(
titleAlignment: ListTileTitleAlignment.top, tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: Platform.isAndroid leading: Platform.isAndroid
? Container( ? Container(
margin: const EdgeInsets.only(top: 4), margin: const EdgeInsets.only(top: 4),
@@ -165,6 +259,10 @@ class RequestItem extends StatelessWidget {
for (final chain in connection.chains) for (final chain in connection.chains)
CommonChip( CommonChip(
label: chain, label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
), ),
], ],
), ),
@@ -176,3 +274,144 @@ class RequestItem extends StatelessWidget {
); );
} }
} }
class RequestsSearchDelegate extends SearchDelegate {
ValueNotifier<ConnectionsAndKeywords> requestsNotifier;
RequestsSearchDelegate({
required ConnectionsAndKeywords state,
}) : requestsNotifier = ValueNotifier<ConnectionsAndKeywords>(state);
get state => requestsNotifier.value;
List<Connection> get _results {
final lowerQuery = query.toLowerCase().trim();
return requestsNotifier.value.filteredConnections.where((request) {
final lowerNetwork = request.metadata.network.toLowerCase();
final lowerHost = request.metadata.host.toLowerCase();
final lowerDestinationIP = request.metadata.destinationIP.toLowerCase();
final lowerProcess = request.metadata.process.toLowerCase();
final lowerChains = request.chains.join("").toLowerCase();
return lowerNetwork.contains(lowerQuery) ||
lowerHost.contains(lowerQuery) ||
lowerDestinationIP.contains(lowerQuery) ||
lowerProcess.contains(lowerQuery) ||
lowerChains.contains(lowerQuery);
}).toList();
}
_addKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..add(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..remove(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
@override
List<Widget>? buildActions(BuildContext context) {
return [
IconButton(
onPressed: () {
if (query.isEmpty) {
close(context, null);
return;
}
query = '';
},
icon: const Icon(Icons.clear),
),
const SizedBox(
width: 8,
)
];
}
@override
Widget? buildLeading(BuildContext context) {
return IconButton(
onPressed: () {
close(context, null);
},
icon: const Icon(Icons.arrow_back),
);
}
@override
Widget buildResults(BuildContext context) {
return buildSuggestions(context);
}
@override
void dispose() {
requestsNotifier.dispose();
super.dispose();
}
@override
Widget buildSuggestions(BuildContext context) {
return ValueListenableBuilder(
valueListenable: requestsNotifier,
builder: (_, __, ___) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.keywords.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
itemBuilder: (_, index) {
final connection = _results[index];
return RequestItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
_addKeyword(value);
},
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: _results.length,
),
)
],
);
},
);
}
}

View File

@@ -78,9 +78,9 @@ class _ResourcesState extends State<Resources> {
const Padding( const Padding(
padding: EdgeInsets.only(left: 12,right: 4), padding: EdgeInsets.only(left: 12,right: 4),
child: VerticalDivider( child: VerticalDivider(
endIndent: 2, endIndent: 6,
width: 4, width: 4,
indent: 2, indent: 6,
), ),
), ),
externalProvider.vehicleType == "HTTP" externalProvider.vehicleType == "HTTP"

View File

@@ -111,9 +111,10 @@ class ThemeFragment extends StatelessWidget {
null, null,
defaultPrimaryColor, defaultPrimaryColor,
Colors.pinkAccent, Colors.pinkAccent,
Colors.lightBlue,
Colors.greenAccent, Colors.greenAccent,
Colors.yellowAccent, Colors.yellowAccent,
Colors.purple Colors.purple,
]; ];
return Column( return Column(
children: [ children: [

View File

@@ -111,7 +111,7 @@
"noMoreInfoDesc": "No more info", "noMoreInfoDesc": "No more info",
"profileParseErrorDesc": "profile parse error", "profileParseErrorDesc": "profile parse error",
"proxyPort": "ProxyPort", "proxyPort": "ProxyPort",
"proxyPortDesc": "Set the clash listening port", "proxyPortDesc": "Set the Clash listening port",
"port": "Port", "port": "Port",
"logLevel": "LogLevel", "logLevel": "LogLevel",
"show": "Show", "show": "Show",
@@ -166,8 +166,8 @@
"allowBypass": "Allow applications to bypass VPN", "allowBypass": "Allow applications to bypass VPN",
"allowBypassDesc": "Some apps can bypass VPN when turned on", "allowBypassDesc": "Some apps can bypass VPN when turned on",
"externalController": "ExternalController", "externalController": "ExternalController",
"externalControllerDesc": "Once enabled, the clash kernel can be controlled on port 9090", "externalControllerDesc": "Once enabled, the Clash kernel can be controlled on port 9090",
"ipv6Desc": "When turned on it will be able to receive ipv6 traffic", "ipv6Desc": "When turned on it will be able to receive IPv6 traffic",
"app": "App", "app": "App",
"general": "General", "general": "General",
"systemProxyDesc": "Attach HTTP proxy to VpnService", "systemProxyDesc": "Attach HTTP proxy to VpnService",
@@ -179,7 +179,18 @@
"geodataLoaderDesc": "Enabling will use the Geo low memory loader", "geodataLoaderDesc": "Enabling will use the Geo low memory loader",
"requests": "Requests", "requests": "Requests",
"requestsDesc": "View recently requested data", "requestsDesc": "View recently requested data",
"nullRequestsDesc": "No proxy or no request",
"findProcessMode": "Find process", "findProcessMode": "Find process",
"findProcessModeDesc": "There is a risk of flashback after opening" "findProcessModeDesc": "There is a risk of flashback after opening",
"init": "Init",
"infiniteTime": "Long term effective",
"expirationTime": "Expiration time",
"connections": "Connections",
"connectionsDesc": "View current connection",
"nullRequestsDesc": "No requests",
"nullConnectionsDesc": "No connections",
"intranetIp": "Intranet IP",
"view": "View",
"cut": "Cut",
"copy": "Copy",
"paste": "Paste"
} }

View File

@@ -111,7 +111,7 @@
"noMoreInfoDesc": "暂无更多信息", "noMoreInfoDesc": "暂无更多信息",
"profileParseErrorDesc": "配置文件解析错误", "profileParseErrorDesc": "配置文件解析错误",
"proxyPort": "代理端口", "proxyPort": "代理端口",
"proxyPortDesc": "设置clash监听端口", "proxyPortDesc": "设置Clash监听端口",
"port": "端口", "port": "端口",
"logLevel": "日志等级", "logLevel": "日志等级",
"show": "显示", "show": "显示",
@@ -163,11 +163,11 @@
"checkError": "检测失败", "checkError": "检测失败",
"ipCheckTimeout": "Ip检测超时", "ipCheckTimeout": "Ip检测超时",
"search": "搜索", "search": "搜索",
"allowBypass": "允许应用绕过vpn", "allowBypass": "允许应用绕过VPN",
"allowBypassDesc": "开启后部分应用可绕过VPN", "allowBypassDesc": "开启后部分应用可绕过VPN",
"externalController": "外部控制器", "externalController": "外部控制器",
"externalControllerDesc": "开启后将可以通过9090端口控制clash内核", "externalControllerDesc": "开启后将可以通过9090端口控制Clash内核",
"ipv6Desc": "开启后将可以接收ipv6流量", "ipv6Desc": "开启后将可以接收IPv6流量",
"app": "应用", "app": "应用",
"general": "基础", "general": "基础",
"systemProxyDesc": "为VpnService附加HTTP代理", "systemProxyDesc": "为VpnService附加HTTP代理",
@@ -179,7 +179,18 @@
"geodataLoaderDesc": "开启将使用Geo低内存加载器", "geodataLoaderDesc": "开启将使用Geo低内存加载器",
"requests": "请求", "requests": "请求",
"requestsDesc": "查看最近请求数据", "requestsDesc": "查看最近请求数据",
"nullRequestsDesc": "未开启代理或者没有请求",
"findProcessMode": "查找进程", "findProcessMode": "查找进程",
"findProcessModeDesc": "开启后存在闪退风险" "findProcessModeDesc": "开启后存在闪退风险",
"init": "初始化",
"infiniteTime": "长期有效",
"expirationTime": "到期时间",
"connections": "连接",
"connectionsDesc": "查看当前连接",
"nullRequestsDesc": "暂无请求",
"nullConnectionsDesc": "暂无连接",
"intranetIp": "内网 IP",
"view": "查看",
"cut": "剪切",
"copy": "复制",
"paste": "粘贴"
} }

View File

@@ -92,11 +92,16 @@ class MessageLookup extends MessageLookupByLibrary {
"compatibleDesc": MessageLookupByLibrary.simpleMessage( "compatibleDesc": MessageLookupByLibrary.simpleMessage(
"Opening it will lose part of its application ability and gain the support of full amount of Clash."), "Opening it will lose part of its application ability and gain the support of full amount of Clash."),
"confirm": MessageLookupByLibrary.simpleMessage("Confirm"), "confirm": MessageLookupByLibrary.simpleMessage("Confirm"),
"connections": MessageLookupByLibrary.simpleMessage("Connections"),
"connectionsDesc":
MessageLookupByLibrary.simpleMessage("View current connection"),
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"), "connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"),
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
"core": MessageLookupByLibrary.simpleMessage("Core"), "core": MessageLookupByLibrary.simpleMessage("Core"),
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"), "coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
"country": MessageLookupByLibrary.simpleMessage("Country"), "country": MessageLookupByLibrary.simpleMessage("Country"),
"create": MessageLookupByLibrary.simpleMessage("Create"), "create": MessageLookupByLibrary.simpleMessage("Create"),
"cut": MessageLookupByLibrary.simpleMessage("Cut"),
"dark": MessageLookupByLibrary.simpleMessage("Dark"), "dark": MessageLookupByLibrary.simpleMessage("Dark"),
"dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"), "dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"),
"days": MessageLookupByLibrary.simpleMessage("Days"), "days": MessageLookupByLibrary.simpleMessage("Days"),
@@ -117,10 +122,12 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("Edit"), "edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"), "en": MessageLookupByLibrary.simpleMessage("English"),
"exit": MessageLookupByLibrary.simpleMessage("Exit"), "exit": MessageLookupByLibrary.simpleMessage("Exit"),
"expirationTime":
MessageLookupByLibrary.simpleMessage("Expiration time"),
"externalController": "externalController":
MessageLookupByLibrary.simpleMessage("ExternalController"), MessageLookupByLibrary.simpleMessage("ExternalController"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage( "externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"Once enabled, the clash kernel can be controlled on port 9090"), "Once enabled, the Clash kernel can be controlled on port 9090"),
"externalResources": "externalResources":
MessageLookupByLibrary.simpleMessage("External resources"), MessageLookupByLibrary.simpleMessage("External resources"),
"file": MessageLookupByLibrary.simpleMessage("File"), "file": MessageLookupByLibrary.simpleMessage("File"),
@@ -142,10 +149,14 @@ class MessageLookup extends MessageLookupByLibrary {
"hours": MessageLookupByLibrary.simpleMessage("Hours"), "hours": MessageLookupByLibrary.simpleMessage("Hours"),
"importFromURL": "importFromURL":
MessageLookupByLibrary.simpleMessage("Import from URL"), MessageLookupByLibrary.simpleMessage("Import from URL"),
"infiniteTime":
MessageLookupByLibrary.simpleMessage("Long term effective"),
"init": MessageLookupByLibrary.simpleMessage("Init"),
"intranetIp": MessageLookupByLibrary.simpleMessage("Intranet IP"),
"ipCheckTimeout": "ipCheckTimeout":
MessageLookupByLibrary.simpleMessage("Ip check timeout"), MessageLookupByLibrary.simpleMessage("Ip check timeout"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage( "ipv6Desc": MessageLookupByLibrary.simpleMessage(
"When turned on it will be able to receive ipv6 traffic"), "When turned on it will be able to receive IPv6 traffic"),
"just": MessageLookupByLibrary.simpleMessage("Just"), "just": MessageLookupByLibrary.simpleMessage("Just"),
"language": MessageLookupByLibrary.simpleMessage("Language"), "language": MessageLookupByLibrary.simpleMessage("Language"),
"light": MessageLookupByLibrary.simpleMessage("Light"), "light": MessageLookupByLibrary.simpleMessage("Light"),
@@ -174,13 +185,14 @@ class MessageLookup extends MessageLookupByLibrary {
"Please create a profile or add a valid profile"), "Please create a profile or add a valid profile"),
"notSelectedTip": MessageLookupByLibrary.simpleMessage( "notSelectedTip": MessageLookupByLibrary.simpleMessage(
"The current proxy group cannot be selected."), "The current proxy group cannot be selected."),
"nullConnectionsDesc":
MessageLookupByLibrary.simpleMessage("No connections"),
"nullCoreInfoDesc": "nullCoreInfoDesc":
MessageLookupByLibrary.simpleMessage("Unable to obtain core info"), MessageLookupByLibrary.simpleMessage("Unable to obtain core info"),
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("No logs"), "nullLogsDesc": MessageLookupByLibrary.simpleMessage("No logs"),
"nullProfileDesc": MessageLookupByLibrary.simpleMessage( "nullProfileDesc": MessageLookupByLibrary.simpleMessage(
"No profile, Please add a profile"), "No profile, Please add a profile"),
"nullRequestsDesc": "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
MessageLookupByLibrary.simpleMessage("No proxy or no request"),
"other": MessageLookupByLibrary.simpleMessage("Other"), "other": MessageLookupByLibrary.simpleMessage("Other"),
"outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"), "outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"),
"override": MessageLookupByLibrary.simpleMessage("Override"), "override": MessageLookupByLibrary.simpleMessage("Override"),
@@ -189,6 +201,7 @@ class MessageLookup extends MessageLookupByLibrary {
"password": MessageLookupByLibrary.simpleMessage("Password"), "password": MessageLookupByLibrary.simpleMessage("Password"),
"passwordTip": "passwordTip":
MessageLookupByLibrary.simpleMessage("Password cannot be empty"), MessageLookupByLibrary.simpleMessage("Password cannot be empty"),
"paste": MessageLookupByLibrary.simpleMessage("Paste"),
"pleaseBindWebDAV": "pleaseBindWebDAV":
MessageLookupByLibrary.simpleMessage("Please bind WebDAV"), MessageLookupByLibrary.simpleMessage("Please bind WebDAV"),
"pleaseUploadFile": "pleaseUploadFile":
@@ -217,7 +230,7 @@ class MessageLookup extends MessageLookupByLibrary {
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"), "proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
"proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"), "proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage( "proxyPortDesc": MessageLookupByLibrary.simpleMessage(
"Set the clash listening port"), "Set the Clash listening port"),
"qrcode": MessageLookupByLibrary.simpleMessage("QR code"), "qrcode": MessageLookupByLibrary.simpleMessage("QR code"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage( "qrcodeDesc": MessageLookupByLibrary.simpleMessage(
"Scan QR code to obtain profile"), "Scan QR code to obtain profile"),
@@ -281,6 +294,7 @@ class MessageLookup extends MessageLookupByLibrary {
"url": MessageLookupByLibrary.simpleMessage("URL"), "url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": "urlDesc":
MessageLookupByLibrary.simpleMessage("Obtain profile through URL"), MessageLookupByLibrary.simpleMessage("Obtain profile through URL"),
"view": MessageLookupByLibrary.simpleMessage("View"),
"webDAVConfiguration": "webDAVConfiguration":
MessageLookupByLibrary.simpleMessage("WebDAV configuration"), MessageLookupByLibrary.simpleMessage("WebDAV configuration"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"), "whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"),

View File

@@ -36,7 +36,7 @@ class MessageLookup extends MessageLookupByLibrary {
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
"ago": MessageLookupByLibrary.simpleMessage(""), "ago": MessageLookupByLibrary.simpleMessage(""),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过vpn"), "allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
"allowBypassDesc": "allowBypassDesc":
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"), MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"), "allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
@@ -75,11 +75,15 @@ class MessageLookup extends MessageLookupByLibrary {
"compatibleDesc": "compatibleDesc":
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"), MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"),
"confirm": MessageLookupByLibrary.simpleMessage("确定"), "confirm": MessageLookupByLibrary.simpleMessage("确定"),
"connections": MessageLookupByLibrary.simpleMessage("连接"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接"),
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"), "connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"copy": MessageLookupByLibrary.simpleMessage("复制"),
"core": MessageLookupByLibrary.simpleMessage("内核"), "core": MessageLookupByLibrary.simpleMessage("内核"),
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"), "coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
"country": MessageLookupByLibrary.simpleMessage("区域"), "country": MessageLookupByLibrary.simpleMessage("区域"),
"create": MessageLookupByLibrary.simpleMessage("创建"), "create": MessageLookupByLibrary.simpleMessage("创建"),
"cut": MessageLookupByLibrary.simpleMessage("剪切"),
"dark": MessageLookupByLibrary.simpleMessage("深色"), "dark": MessageLookupByLibrary.simpleMessage("深色"),
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"), "dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
"days": MessageLookupByLibrary.simpleMessage(""), "days": MessageLookupByLibrary.simpleMessage(""),
@@ -97,9 +101,10 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("编辑"), "edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"), "en": MessageLookupByLibrary.simpleMessage("英语"),
"exit": MessageLookupByLibrary.simpleMessage("退出"), "exit": MessageLookupByLibrary.simpleMessage("退出"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"), "externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc": "externalControllerDesc":
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制clash内核"), MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"), "externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"file": MessageLookupByLibrary.simpleMessage("文件"), "file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), "fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
@@ -116,8 +121,11 @@ class MessageLookup extends MessageLookupByLibrary {
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"), "goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hours": MessageLookupByLibrary.simpleMessage("小时"), "hours": MessageLookupByLibrary.simpleMessage("小时"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"), "importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"init": MessageLookupByLibrary.simpleMessage("初始化"),
"intranetIp": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"), "ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收ipv6流量"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"), "just": MessageLookupByLibrary.simpleMessage("刚刚"),
"language": MessageLookupByLibrary.simpleMessage("语言"), "language": MessageLookupByLibrary.simpleMessage("语言"),
"light": MessageLookupByLibrary.simpleMessage("浅色"), "light": MessageLookupByLibrary.simpleMessage("浅色"),
@@ -142,17 +150,19 @@ class MessageLookup extends MessageLookupByLibrary {
"noProxyDesc": "noProxyDesc":
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"), MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
"notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"), "notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"),
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"),
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"), "nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"),
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"), "nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
"nullProfileDesc": "nullProfileDesc":
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("未开启代理或者没有请求"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"other": MessageLookupByLibrary.simpleMessage("其他"), "other": MessageLookupByLibrary.simpleMessage("其他"),
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
"override": MessageLookupByLibrary.simpleMessage("覆写"), "override": MessageLookupByLibrary.simpleMessage("覆写"),
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"), "overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
"password": MessageLookupByLibrary.simpleMessage("密码"), "password": MessageLookupByLibrary.simpleMessage("密码"),
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"), "passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"), "pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"), "pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
"pleaseUploadValidQrcode": "pleaseUploadValidQrcode":
@@ -176,7 +186,7 @@ class MessageLookup extends MessageLookupByLibrary {
"project": MessageLookupByLibrary.simpleMessage("项目"), "project": MessageLookupByLibrary.simpleMessage("项目"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"), "proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置clash监听端口"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"),
@@ -226,6 +236,7 @@ class MessageLookup extends MessageLookupByLibrary {
"upload": MessageLookupByLibrary.simpleMessage("上传"), "upload": MessageLookupByLibrary.simpleMessage("上传"),
"url": MessageLookupByLibrary.simpleMessage("URL"), "url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), "urlDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"view": MessageLookupByLibrary.simpleMessage("查看"),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"), "whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),
"years": MessageLookupByLibrary.simpleMessage(""), "years": MessageLookupByLibrary.simpleMessage(""),

View File

@@ -1170,10 +1170,10 @@ class AppLocalizations {
); );
} }
/// `Set the clash listening port` /// `Set the Clash listening port`
String get proxyPortDesc { String get proxyPortDesc {
return Intl.message( return Intl.message(
'Set the clash listening port', 'Set the Clash listening port',
name: 'proxyPortDesc', name: 'proxyPortDesc',
desc: '', desc: '',
args: [], args: [],
@@ -1720,20 +1720,20 @@ class AppLocalizations {
); );
} }
/// `Once enabled, the clash kernel can be controlled on port 9090` /// `Once enabled, the Clash kernel can be controlled on port 9090`
String get externalControllerDesc { String get externalControllerDesc {
return Intl.message( return Intl.message(
'Once enabled, the clash kernel can be controlled on port 9090', 'Once enabled, the Clash kernel can be controlled on port 9090',
name: 'externalControllerDesc', name: 'externalControllerDesc',
desc: '', desc: '',
args: [], args: [],
); );
} }
/// `When turned on it will be able to receive ipv6 traffic` /// `When turned on it will be able to receive IPv6 traffic`
String get ipv6Desc { String get ipv6Desc {
return Intl.message( return Intl.message(
'When turned on it will be able to receive ipv6 traffic', 'When turned on it will be able to receive IPv6 traffic',
name: 'ipv6Desc', name: 'ipv6Desc',
desc: '', desc: '',
args: [], args: [],
@@ -1850,16 +1850,6 @@ class AppLocalizations {
); );
} }
/// `No proxy or no request`
String get nullRequestsDesc {
return Intl.message(
'No proxy or no request',
name: 'nullRequestsDesc',
desc: '',
args: [],
);
}
/// `Find process` /// `Find process`
String get findProcessMode { String get findProcessMode {
return Intl.message( return Intl.message(
@@ -1879,6 +1869,126 @@ class AppLocalizations {
args: [], args: [],
); );
} }
/// `Init`
String get init {
return Intl.message(
'Init',
name: 'init',
desc: '',
args: [],
);
}
/// `Long term effective`
String get infiniteTime {
return Intl.message(
'Long term effective',
name: 'infiniteTime',
desc: '',
args: [],
);
}
/// `Expiration time`
String get expirationTime {
return Intl.message(
'Expiration time',
name: 'expirationTime',
desc: '',
args: [],
);
}
/// `Connections`
String get connections {
return Intl.message(
'Connections',
name: 'connections',
desc: '',
args: [],
);
}
/// `View current connection`
String get connectionsDesc {
return Intl.message(
'View current connection',
name: 'connectionsDesc',
desc: '',
args: [],
);
}
/// `No requests`
String get nullRequestsDesc {
return Intl.message(
'No requests',
name: 'nullRequestsDesc',
desc: '',
args: [],
);
}
/// `No connections`
String get nullConnectionsDesc {
return Intl.message(
'No connections',
name: 'nullConnectionsDesc',
desc: '',
args: [],
);
}
/// `Intranet IP`
String get intranetIp {
return Intl.message(
'Intranet IP',
name: 'intranetIp',
desc: '',
args: [],
);
}
/// `View`
String get view {
return Intl.message(
'View',
name: 'view',
desc: '',
args: [],
);
}
/// `Cut`
String get cut {
return Intl.message(
'Cut',
name: 'cut',
desc: '',
args: [],
);
}
/// `Copy`
String get copy {
return Intl.message(
'Copy',
name: 'copy',
desc: '',
args: [],
);
}
/// `Paste`
String get paste {
return Intl.message(
'Paste',
name: 'paste',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -54,21 +54,18 @@ Future<void> vpnService() async {
int.parse(fd), int.parse(fd),
); );
})); }));
await globalState.init( await globalState.init(
appState: appState, appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
if (appState.isInit) { globalState.applyProfile(
await globalState.applyProfile( appState: appState,
appState: appState, config: config,
config: config, clashConfig: clashConfig,
clashConfig: clashConfig, );
);
} else {
exit(0);
}
final appLocalizations = await AppLocalizations.load( final appLocalizations = await AppLocalizations.load(
other.getLocaleForString(config.locale) ?? other.getLocaleForString(config.locale) ??
@@ -78,6 +75,7 @@ Future<void> vpnService() async {
handleStart() async { handleStart() async {
await app?.tip(appLocalizations.startVpn); await app?.tip(appLocalizations.startVpn);
await globalState.startSystemProxy( await globalState.startSystemProxy(
appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
@@ -89,19 +87,18 @@ Future<void> vpnService() async {
]; ];
} }
if (appState.isInit) { handleStart();
handleStart();
tile?.addListener( tile?.addListener(
TileListenerWithVpn( TileListenerWithVpn(
onStop: () async { onStop: () async {
await app?.tip(appLocalizations.stopVpn); await app?.tip(appLocalizations.stopVpn);
await globalState.stopSystemProxy(); await globalState.stopSystemProxy();
clashCore.shutdown(); clashCore.shutdown();
exit(0); exit(0);
}, },
), ),
); );
}
} }
class ClashMessageListenerWithVpn with ClashMessageListener { class ClashMessageListenerWithVpn with ClashMessageListener {

View File

@@ -22,6 +22,7 @@ class AppState with ChangeNotifier {
bool _isInit; bool _isInit;
VersionInfo? _versionInfo; VersionInfo? _versionInfo;
List<Traffic> _traffics; List<Traffic> _traffics;
Traffic _totalTraffic;
List<Log> _logs; List<Log> _logs;
String _currentLabel; String _currentLabel;
SystemColorSchemes _systemColorSchemes; SystemColorSchemes _systemColorSchemes;
@@ -48,6 +49,7 @@ class AppState with ChangeNotifier {
_sortNum = 0, _sortNum = 0,
_requests = [], _requests = [],
_mode = mode, _mode = mode,
_totalTraffic = Traffic(),
_delayMap = {}, _delayMap = {},
_groups = [], _groups = [],
_isCompatible = isCompatible, _isCompatible = isCompatible,
@@ -157,11 +159,24 @@ class AppState with ChangeNotifier {
} }
} }
addTraffic(Traffic value) { addTraffic(Traffic traffic) {
_traffics = List.from(_traffics)..add(value); _traffics = List.from(_traffics)..add(traffic);
const maxLength = 60;
if (_traffics.length > maxLength) {
_traffics = _traffics.sublist(_traffics.length - maxLength);
}
notifyListeners(); notifyListeners();
} }
Traffic get totalTraffic => _totalTraffic;
set totalTraffic(Traffic value) {
if (_totalTraffic != value) {
_totalTraffic = value;
notifyListeners();
}
}
List<Connection> get requests => _requests; List<Connection> get requests => _requests;
set requests(List<Connection> value) { set requests(List<Connection> value) {
@@ -172,7 +187,7 @@ class AppState with ChangeNotifier {
} }
addRequest(Connection value) { addRequest(Connection value) {
_requests.add(value); _requests = List.from(_requests)..add(value);
final maxLength = Platform.isAndroid ? 1000 : 60; final maxLength = Platform.isAndroid ? 1000 : 60;
if (_requests.length > maxLength) { if (_requests.length > maxLength) {
_requests = _requests.sublist(_requests.length - maxLength); _requests = _requests.sublist(_requests.length - maxLength);
@@ -190,11 +205,10 @@ class AppState with ChangeNotifier {
} }
addLog(Log log) { addLog(Log log) {
_logs.add(log); _logs = List.from(_logs)..add(log);
if (!Platform.isAndroid) { final maxLength = Platform.isAndroid ? 1000 : 60;
if (_logs.length > 60) { if (_logs.length > maxLength) {
_logs = _logs.sublist(_logs.length - 60); _logs = _logs.sublist(_logs.length - maxLength);
}
} }
notifyListeners(); notifyListeners();
} }

View File

@@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@@ -56,6 +57,9 @@ class Config extends ChangeNotifier {
bool _allowBypass; bool _allowBypass;
bool _systemProxy; bool _systemProxy;
DAV? _dav; DAV? _dav;
ProxiesType _proxiesType;
ProxyCardType _proxyCardType;
int _proxiesColumns;
Config() Config()
: _profiles = [], : _profiles = [],
@@ -70,10 +74,13 @@ class Config extends ChangeNotifier {
_isMinimizeOnExit = true, _isMinimizeOnExit = true,
_isAccessControl = false, _isAccessControl = false,
_autoCheckUpdate = true, _autoCheckUpdate = true,
_systemProxy = false, _systemProxy = true,
_accessControl = const AccessControl(), _accessControl = const AccessControl(),
_isAnimateToPage = true, _isAnimateToPage = true,
_allowBypass = true; _allowBypass = true,
_proxyCardType = ProxyCardType.expand,
_proxiesType = ProxiesType.tab,
_proxiesColumns = 2;
deleteProfileById(String id) { deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList(); _profiles = profiles.where((element) => element.id != id).toList();
@@ -143,18 +150,34 @@ class Config extends ChangeNotifier {
} }
Profile? get currentProfile { Profile? get currentProfile {
try { final index =
return profiles.firstWhere((element) => element.id == _currentProfileId); profiles.indexWhere((profile) => profile.id == _currentProfileId);
} catch (_) { return index == -1 ? null : profiles[index];
return null;
}
} }
String? get currentGroupName => currentProfile?.currentGroupName; String? get currentGroupName => currentProfile?.currentGroupName;
Set<String> get currentUnfoldSet => currentProfile?.unfoldSet ?? {};
updateCurrentUnfoldSet(Set<String> value) {
if (!const SetEquality<String>().equals(currentUnfoldSet, value)) {
_setProfile(
currentProfile!.copyWith(
unfoldSet: value,
),
);
notifyListeners();
}
}
updateCurrentGroupName(String groupName) { updateCurrentGroupName(String groupName) {
if (currentProfile?.currentGroupName != groupName) { if (currentProfile != null &&
currentProfile?.currentGroupName = groupName; currentProfile!.currentGroupName != groupName) {
_setProfile(
currentProfile!.copyWith(
currentGroupName: groupName,
),
);
notifyListeners(); notifyListeners();
} }
} }
@@ -164,9 +187,16 @@ class Config extends ChangeNotifier {
} }
updateCurrentSelectedMap(String groupName, String proxyName) { updateCurrentSelectedMap(String groupName, String proxyName) {
if (currentProfile?.selectedMap[groupName] != proxyName) { if (currentProfile != null &&
currentProfile?.selectedMap = Map.from(currentProfile?.selectedMap ?? {}) currentProfile!.selectedMap[groupName] != proxyName) {
..[groupName] = proxyName; final SelectedMap selectedMap = Map.from(
currentProfile?.selectedMap ?? {},
)..[groupName] = proxyName;
_setProfile(
currentProfile!.copyWith(
selectedMap: selectedMap,
),
);
notifyListeners(); notifyListeners();
} }
} }
@@ -354,6 +384,36 @@ class Config extends ChangeNotifier {
} }
} }
@JsonKey(defaultValue: ProxiesType.tab)
ProxiesType get proxiesType => _proxiesType;
set proxiesType(ProxiesType value) {
if (_proxiesType != value) {
_proxiesType = value;
notifyListeners();
}
}
@JsonKey(defaultValue: ProxyCardType.expand)
ProxyCardType get proxyCardType => _proxyCardType;
set proxyCardType(ProxyCardType value) {
if (_proxyCardType != value) {
_proxyCardType = value;
notifyListeners();
}
}
@JsonKey(defaultValue: 2)
int get proxiesColumns => _proxiesColumns;
set proxiesColumns(int value) {
if (_proxiesColumns != value) {
_proxiesColumns = value;
notifyListeners();
}
}
update([ update([
Config? config, Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all, RecoveryOption recoveryOptions = RecoveryOption.all,
@@ -373,6 +433,7 @@ class Config extends ChangeNotifier {
_autoLaunch = config._autoLaunch; _autoLaunch = config._autoLaunch;
_silentLaunch = config._silentLaunch; _silentLaunch = config._silentLaunch;
_autoRun = config._autoRun; _autoRun = config._autoRun;
_proxiesType = config._proxiesType;
_openLog = config._openLog; _openLog = config._openLog;
_themeMode = config._themeMode; _themeMode = config._themeMode;
_locale = config._locale; _locale = config._locale;

View File

@@ -23,7 +23,7 @@ class Metadata with _$Metadata {
} }
@freezed @freezed
class Connection with _$Connection{ class Connection with _$Connection {
const factory Connection({ const factory Connection({
required String id, required String id,
num? upload, num? upload,
@@ -36,3 +36,19 @@ class Connection with _$Connection{
factory Connection.fromJson(Map<String, Object?> json) => factory Connection.fromJson(Map<String, Object?> json) =>
_$ConnectionFromJson(json); _$ConnectionFromJson(json);
} }
@freezed
class ConnectionsAndKeywords with _$ConnectionsAndKeywords {
const factory ConnectionsAndKeywords({
@Default([]) List<Connection> connections,
@Default([]) List<String> keywords,
}) = _ConnectionsAndKeywords;
factory ConnectionsAndKeywords.fromJson(Map<String, Object?> json) =>
_$ConnectionsAndKeywordsFromJson(json);
}
extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords{
List<Connection> get filteredConnections => connections.where((connection)=> Set.from(connection.chains).containsAll(keywords)).toList();
}

View File

@@ -79,7 +79,7 @@ class Process with _$Process {
class ProcessMapItem with _$ProcessMapItem { class ProcessMapItem with _$ProcessMapItem {
const factory ProcessMapItem({ const factory ProcessMapItem({
required int id, required int id,
String? value, required String value,
}) = _ProcessMapItem; }) = _ProcessMapItem;
factory ProcessMapItem.fromJson(Map<String, Object?> json) => factory ProcessMapItem.fromJson(Map<String, Object?> json) =>

View File

@@ -34,7 +34,14 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..isCompatible = json['isCompatible'] as bool? ?? true ..isCompatible = json['isCompatible'] as bool? ?? true
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true ..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
..allowBypass = json['allowBypass'] as bool? ?? true ..allowBypass = json['allowBypass'] as bool? ?? true
..systemProxy = json['systemProxy'] as bool? ?? true; ..systemProxy = json['systemProxy'] as bool? ?? true
..proxiesType =
$enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType']) ??
ProxiesType.tab
..proxyCardType =
$enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ??
ProxyCardType.expand
..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2;
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles, 'profiles': instance.profiles,
@@ -56,6 +63,9 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'autoCheckUpdate': instance.autoCheckUpdate, 'autoCheckUpdate': instance.autoCheckUpdate,
'allowBypass': instance.allowBypass, 'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy, 'systemProxy': instance.systemProxy,
'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!,
'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!,
'proxiesColumns': instance.proxiesColumns,
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {
@@ -70,6 +80,16 @@ const _$ProxiesSortTypeEnumMap = {
ProxiesSortType.name: 'name', ProxiesSortType.name: 'name',
}; };
const _$ProxiesTypeEnumMap = {
ProxiesType.tab: 'tab',
ProxiesType.expansion: 'expansion',
};
const _$ProxyCardTypeEnumMap = {
ProxyCardType.expand: 'expand',
ProxyCardType.shrink: 'shrink',
};
_$AccessControlImpl _$$AccessControlImplFromJson(Map<String, dynamic> json) => _$AccessControlImpl _$$AccessControlImplFromJson(Map<String, dynamic> json) =>
_$AccessControlImpl( _$AccessControlImpl(
mode: $enumDecodeNullable(_$AccessControlModeEnumMap, json['mode']) ?? mode: $enumDecodeNullable(_$AccessControlModeEnumMap, json['mode']) ??

View File

@@ -589,3 +589,184 @@ abstract class _Connection implements Connection {
_$$ConnectionImplCopyWith<_$ConnectionImpl> get copyWith => _$$ConnectionImplCopyWith<_$ConnectionImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
ConnectionsAndKeywords _$ConnectionsAndKeywordsFromJson(
Map<String, dynamic> json) {
return _ConnectionsAndKeywords.fromJson(json);
}
/// @nodoc
mixin _$ConnectionsAndKeywords {
List<Connection> get connections => throw _privateConstructorUsedError;
List<String> get keywords => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ConnectionsAndKeywordsCopyWith<ConnectionsAndKeywords> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ConnectionsAndKeywordsCopyWith<$Res> {
factory $ConnectionsAndKeywordsCopyWith(ConnectionsAndKeywords value,
$Res Function(ConnectionsAndKeywords) then) =
_$ConnectionsAndKeywordsCopyWithImpl<$Res, ConnectionsAndKeywords>;
@useResult
$Res call({List<Connection> connections, List<String> keywords});
}
/// @nodoc
class _$ConnectionsAndKeywordsCopyWithImpl<$Res,
$Val extends ConnectionsAndKeywords>
implements $ConnectionsAndKeywordsCopyWith<$Res> {
_$ConnectionsAndKeywordsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? connections = null,
Object? keywords = null,
}) {
return _then(_value.copyWith(
connections: null == connections
? _value.connections
: connections // ignore: cast_nullable_to_non_nullable
as List<Connection>,
keywords: null == keywords
? _value.keywords
: keywords // ignore: cast_nullable_to_non_nullable
as List<String>,
) as $Val);
}
}
/// @nodoc
abstract class _$$ConnectionsAndKeywordsImplCopyWith<$Res>
implements $ConnectionsAndKeywordsCopyWith<$Res> {
factory _$$ConnectionsAndKeywordsImplCopyWith(
_$ConnectionsAndKeywordsImpl value,
$Res Function(_$ConnectionsAndKeywordsImpl) then) =
__$$ConnectionsAndKeywordsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<Connection> connections, List<String> keywords});
}
/// @nodoc
class __$$ConnectionsAndKeywordsImplCopyWithImpl<$Res>
extends _$ConnectionsAndKeywordsCopyWithImpl<$Res,
_$ConnectionsAndKeywordsImpl>
implements _$$ConnectionsAndKeywordsImplCopyWith<$Res> {
__$$ConnectionsAndKeywordsImplCopyWithImpl(
_$ConnectionsAndKeywordsImpl _value,
$Res Function(_$ConnectionsAndKeywordsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? connections = null,
Object? keywords = null,
}) {
return _then(_$ConnectionsAndKeywordsImpl(
connections: null == connections
? _value._connections
: connections // ignore: cast_nullable_to_non_nullable
as List<Connection>,
keywords: null == keywords
? _value._keywords
: keywords // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ConnectionsAndKeywordsImpl implements _ConnectionsAndKeywords {
const _$ConnectionsAndKeywordsImpl(
{final List<Connection> connections = const [],
final List<String> keywords = const []})
: _connections = connections,
_keywords = keywords;
factory _$ConnectionsAndKeywordsImpl.fromJson(Map<String, dynamic> json) =>
_$$ConnectionsAndKeywordsImplFromJson(json);
final List<Connection> _connections;
@override
@JsonKey()
List<Connection> get connections {
if (_connections is EqualUnmodifiableListView) return _connections;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_connections);
}
final List<String> _keywords;
@override
@JsonKey()
List<String> get keywords {
if (_keywords is EqualUnmodifiableListView) return _keywords;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_keywords);
}
@override
String toString() {
return 'ConnectionsAndKeywords(connections: $connections, keywords: $keywords)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ConnectionsAndKeywordsImpl &&
const DeepCollectionEquality()
.equals(other._connections, _connections) &&
const DeepCollectionEquality().equals(other._keywords, _keywords));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_connections),
const DeepCollectionEquality().hash(_keywords));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ConnectionsAndKeywordsImplCopyWith<_$ConnectionsAndKeywordsImpl>
get copyWith => __$$ConnectionsAndKeywordsImplCopyWithImpl<
_$ConnectionsAndKeywordsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ConnectionsAndKeywordsImplToJson(
this,
);
}
}
abstract class _ConnectionsAndKeywords implements ConnectionsAndKeywords {
const factory _ConnectionsAndKeywords(
{final List<Connection> connections,
final List<String> keywords}) = _$ConnectionsAndKeywordsImpl;
factory _ConnectionsAndKeywords.fromJson(Map<String, dynamic> json) =
_$ConnectionsAndKeywordsImpl.fromJson;
@override
List<Connection> get connections;
@override
List<String> get keywords;
@override
@JsonKey(ignore: true)
_$$ConnectionsAndKeywordsImplCopyWith<_$ConnectionsAndKeywordsImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -52,3 +52,23 @@ Map<String, dynamic> _$$ConnectionImplToJson(_$ConnectionImpl instance) =>
'metadata': instance.metadata, 'metadata': instance.metadata,
'chains': instance.chains, 'chains': instance.chains,
}; };
_$ConnectionsAndKeywordsImpl _$$ConnectionsAndKeywordsImplFromJson(
Map<String, dynamic> json) =>
_$ConnectionsAndKeywordsImpl(
connections: (json['connections'] as List<dynamic>?)
?.map((e) => Connection.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
keywords: (json['keywords'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$$ConnectionsAndKeywordsImplToJson(
_$ConnectionsAndKeywordsImpl instance) =>
<String, dynamic>{
'connections': instance.connections,
'keywords': instance.keywords,
};

View File

@@ -1013,7 +1013,7 @@ ProcessMapItem _$ProcessMapItemFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$ProcessMapItem { mixin _$ProcessMapItem {
int get id => throw _privateConstructorUsedError; int get id => throw _privateConstructorUsedError;
String? get value => throw _privateConstructorUsedError; String get value => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@@ -1027,7 +1027,7 @@ abstract class $ProcessMapItemCopyWith<$Res> {
ProcessMapItem value, $Res Function(ProcessMapItem) then) = ProcessMapItem value, $Res Function(ProcessMapItem) then) =
_$ProcessMapItemCopyWithImpl<$Res, ProcessMapItem>; _$ProcessMapItemCopyWithImpl<$Res, ProcessMapItem>;
@useResult @useResult
$Res call({int id, String? value}); $Res call({int id, String value});
} }
/// @nodoc /// @nodoc
@@ -1044,17 +1044,17 @@ class _$ProcessMapItemCopyWithImpl<$Res, $Val extends ProcessMapItem>
@override @override
$Res call({ $Res call({
Object? id = null, Object? id = null,
Object? value = freezed, Object? value = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
id: null == id id: null == id
? _value.id ? _value.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as int, as int,
value: freezed == value value: null == value
? _value.value ? _value.value
: value // ignore: cast_nullable_to_non_nullable : value // ignore: cast_nullable_to_non_nullable
as String?, as String,
) as $Val); ) as $Val);
} }
} }
@@ -1067,7 +1067,7 @@ abstract class _$$ProcessMapItemImplCopyWith<$Res>
__$$ProcessMapItemImplCopyWithImpl<$Res>; __$$ProcessMapItemImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({int id, String? value}); $Res call({int id, String value});
} }
/// @nodoc /// @nodoc
@@ -1082,17 +1082,17 @@ class __$$ProcessMapItemImplCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? id = null, Object? id = null,
Object? value = freezed, Object? value = null,
}) { }) {
return _then(_$ProcessMapItemImpl( return _then(_$ProcessMapItemImpl(
id: null == id id: null == id
? _value.id ? _value.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as int, as int,
value: freezed == value value: null == value
? _value.value ? _value.value
: value // ignore: cast_nullable_to_non_nullable : value // ignore: cast_nullable_to_non_nullable
as String?, as String,
)); ));
} }
} }
@@ -1100,7 +1100,7 @@ class __$$ProcessMapItemImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$ProcessMapItemImpl implements _ProcessMapItem { class _$ProcessMapItemImpl implements _ProcessMapItem {
const _$ProcessMapItemImpl({required this.id, this.value}); const _$ProcessMapItemImpl({required this.id, required this.value});
factory _$ProcessMapItemImpl.fromJson(Map<String, dynamic> json) => factory _$ProcessMapItemImpl.fromJson(Map<String, dynamic> json) =>
_$$ProcessMapItemImplFromJson(json); _$$ProcessMapItemImplFromJson(json);
@@ -1108,7 +1108,7 @@ class _$ProcessMapItemImpl implements _ProcessMapItem {
@override @override
final int id; final int id;
@override @override
final String? value; final String value;
@override @override
String toString() { String toString() {
@@ -1144,8 +1144,9 @@ class _$ProcessMapItemImpl implements _ProcessMapItem {
} }
abstract class _ProcessMapItem implements ProcessMapItem { abstract class _ProcessMapItem implements ProcessMapItem {
const factory _ProcessMapItem({required final int id, final String? value}) = const factory _ProcessMapItem(
_$ProcessMapItemImpl; {required final int id,
required final String value}) = _$ProcessMapItemImpl;
factory _ProcessMapItem.fromJson(Map<String, dynamic> json) = factory _ProcessMapItem.fromJson(Map<String, dynamic> json) =
_$ProcessMapItemImpl.fromJson; _$ProcessMapItemImpl.fromJson;
@@ -1153,7 +1154,7 @@ abstract class _ProcessMapItem implements ProcessMapItem {
@override @override
int get id; int get id;
@override @override
String? get value; String get value;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$ProcessMapItemImplCopyWith<_$ProcessMapItemImpl> get copyWith => _$$ProcessMapItemImplCopyWith<_$ProcessMapItemImpl> get copyWith =>

View File

@@ -57,6 +57,7 @@ const _$MessageTypeEnumMap = {
MessageType.process: 'process', MessageType.process: 'process',
MessageType.now: 'now', MessageType.now: 'now',
MessageType.request: 'request', MessageType.request: 'request',
MessageType.run: 'run',
}; };
_$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl( _$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl(
@@ -95,7 +96,7 @@ Map<String, dynamic> _$$ProcessImplToJson(_$ProcessImpl instance) =>
_$ProcessMapItemImpl _$$ProcessMapItemImplFromJson(Map<String, dynamic> json) => _$ProcessMapItemImpl _$$ProcessMapItemImplFromJson(Map<String, dynamic> json) =>
_$ProcessMapItemImpl( _$ProcessMapItemImpl(
id: (json['id'] as num).toInt(), id: (json['id'] as num).toInt(),
value: json['value'] as String?, value: json['value'] as String,
); );
Map<String, dynamic> _$$ProcessMapItemImplToJson( Map<String, dynamic> _$$ProcessMapItemImplToJson(

View File

@@ -0,0 +1,189 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of '../log.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
LogsAndKeywords _$LogsAndKeywordsFromJson(Map<String, dynamic> json) {
return _LogsAndKeywords.fromJson(json);
}
/// @nodoc
mixin _$LogsAndKeywords {
List<Log> get logs => throw _privateConstructorUsedError;
List<String> get keywords => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LogsAndKeywordsCopyWith<LogsAndKeywords> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LogsAndKeywordsCopyWith<$Res> {
factory $LogsAndKeywordsCopyWith(
LogsAndKeywords value, $Res Function(LogsAndKeywords) then) =
_$LogsAndKeywordsCopyWithImpl<$Res, LogsAndKeywords>;
@useResult
$Res call({List<Log> logs, List<String> keywords});
}
/// @nodoc
class _$LogsAndKeywordsCopyWithImpl<$Res, $Val extends LogsAndKeywords>
implements $LogsAndKeywordsCopyWith<$Res> {
_$LogsAndKeywordsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? logs = null,
Object? keywords = null,
}) {
return _then(_value.copyWith(
logs: null == logs
? _value.logs
: logs // ignore: cast_nullable_to_non_nullable
as List<Log>,
keywords: null == keywords
? _value.keywords
: keywords // ignore: cast_nullable_to_non_nullable
as List<String>,
) as $Val);
}
}
/// @nodoc
abstract class _$$LogsAndKeywordsImplCopyWith<$Res>
implements $LogsAndKeywordsCopyWith<$Res> {
factory _$$LogsAndKeywordsImplCopyWith(_$LogsAndKeywordsImpl value,
$Res Function(_$LogsAndKeywordsImpl) then) =
__$$LogsAndKeywordsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<Log> logs, List<String> keywords});
}
/// @nodoc
class __$$LogsAndKeywordsImplCopyWithImpl<$Res>
extends _$LogsAndKeywordsCopyWithImpl<$Res, _$LogsAndKeywordsImpl>
implements _$$LogsAndKeywordsImplCopyWith<$Res> {
__$$LogsAndKeywordsImplCopyWithImpl(
_$LogsAndKeywordsImpl _value, $Res Function(_$LogsAndKeywordsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? logs = null,
Object? keywords = null,
}) {
return _then(_$LogsAndKeywordsImpl(
logs: null == logs
? _value._logs
: logs // ignore: cast_nullable_to_non_nullable
as List<Log>,
keywords: null == keywords
? _value._keywords
: keywords // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$LogsAndKeywordsImpl implements _LogsAndKeywords {
const _$LogsAndKeywordsImpl(
{final List<Log> logs = const [], final List<String> keywords = const []})
: _logs = logs,
_keywords = keywords;
factory _$LogsAndKeywordsImpl.fromJson(Map<String, dynamic> json) =>
_$$LogsAndKeywordsImplFromJson(json);
final List<Log> _logs;
@override
@JsonKey()
List<Log> get logs {
if (_logs is EqualUnmodifiableListView) return _logs;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_logs);
}
final List<String> _keywords;
@override
@JsonKey()
List<String> get keywords {
if (_keywords is EqualUnmodifiableListView) return _keywords;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_keywords);
}
@override
String toString() {
return 'LogsAndKeywords(logs: $logs, keywords: $keywords)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LogsAndKeywordsImpl &&
const DeepCollectionEquality().equals(other._logs, _logs) &&
const DeepCollectionEquality().equals(other._keywords, _keywords));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_logs),
const DeepCollectionEquality().hash(_keywords));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$LogsAndKeywordsImplCopyWith<_$LogsAndKeywordsImpl> get copyWith =>
__$$LogsAndKeywordsImplCopyWithImpl<_$LogsAndKeywordsImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$LogsAndKeywordsImplToJson(
this,
);
}
}
abstract class _LogsAndKeywords implements LogsAndKeywords {
const factory _LogsAndKeywords(
{final List<Log> logs,
final List<String> keywords}) = _$LogsAndKeywordsImpl;
factory _LogsAndKeywords.fromJson(Map<String, dynamic> json) =
_$LogsAndKeywordsImpl.fromJson;
@override
List<Log> get logs;
@override
List<String> get keywords;
@override
@JsonKey(ignore: true)
_$$LogsAndKeywordsImplCopyWith<_$LogsAndKeywordsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -23,3 +23,23 @@ const _$LogLevelEnumMap = {
LogLevel.error: 'error', LogLevel.error: 'error',
LogLevel.silent: 'silent', LogLevel.silent: 'silent',
}; };
_$LogsAndKeywordsImpl _$$LogsAndKeywordsImplFromJson(
Map<String, dynamic> json) =>
_$LogsAndKeywordsImpl(
logs: (json['logs'] as List<dynamic>?)
?.map((e) => Log.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
keywords: (json['keywords'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$$LogsAndKeywordsImplToJson(
_$LogsAndKeywordsImpl instance) =>
<String, dynamic>{
'logs': instance.logs,
'keywords': instance.keywords,
};

View File

@@ -0,0 +1,576 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of '../profile.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
UserInfo _$UserInfoFromJson(Map<String, dynamic> json) {
return _UserInfo.fromJson(json);
}
/// @nodoc
mixin _$UserInfo {
int get upload => throw _privateConstructorUsedError;
int get download => throw _privateConstructorUsedError;
int get total => throw _privateConstructorUsedError;
int get expire => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$UserInfoCopyWith<UserInfo> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserInfoCopyWith<$Res> {
factory $UserInfoCopyWith(UserInfo value, $Res Function(UserInfo) then) =
_$UserInfoCopyWithImpl<$Res, UserInfo>;
@useResult
$Res call({int upload, int download, int total, int expire});
}
/// @nodoc
class _$UserInfoCopyWithImpl<$Res, $Val extends UserInfo>
implements $UserInfoCopyWith<$Res> {
_$UserInfoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? upload = null,
Object? download = null,
Object? total = null,
Object? expire = null,
}) {
return _then(_value.copyWith(
upload: null == upload
? _value.upload
: upload // ignore: cast_nullable_to_non_nullable
as int,
download: null == download
? _value.download
: download // ignore: cast_nullable_to_non_nullable
as int,
total: null == total
? _value.total
: total // ignore: cast_nullable_to_non_nullable
as int,
expire: null == expire
? _value.expire
: expire // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$UserInfoImplCopyWith<$Res>
implements $UserInfoCopyWith<$Res> {
factory _$$UserInfoImplCopyWith(
_$UserInfoImpl value, $Res Function(_$UserInfoImpl) then) =
__$$UserInfoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int upload, int download, int total, int expire});
}
/// @nodoc
class __$$UserInfoImplCopyWithImpl<$Res>
extends _$UserInfoCopyWithImpl<$Res, _$UserInfoImpl>
implements _$$UserInfoImplCopyWith<$Res> {
__$$UserInfoImplCopyWithImpl(
_$UserInfoImpl _value, $Res Function(_$UserInfoImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? upload = null,
Object? download = null,
Object? total = null,
Object? expire = null,
}) {
return _then(_$UserInfoImpl(
upload: null == upload
? _value.upload
: upload // ignore: cast_nullable_to_non_nullable
as int,
download: null == download
? _value.download
: download // ignore: cast_nullable_to_non_nullable
as int,
total: null == total
? _value.total
: total // ignore: cast_nullable_to_non_nullable
as int,
expire: null == expire
? _value.expire
: expire // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$UserInfoImpl implements _UserInfo {
const _$UserInfoImpl(
{this.upload = 0, this.download = 0, this.total = 0, this.expire = 0});
factory _$UserInfoImpl.fromJson(Map<String, dynamic> json) =>
_$$UserInfoImplFromJson(json);
@override
@JsonKey()
final int upload;
@override
@JsonKey()
final int download;
@override
@JsonKey()
final int total;
@override
@JsonKey()
final int expire;
@override
String toString() {
return 'UserInfo(upload: $upload, download: $download, total: $total, expire: $expire)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserInfoImpl &&
(identical(other.upload, upload) || other.upload == upload) &&
(identical(other.download, download) ||
other.download == download) &&
(identical(other.total, total) || other.total == total) &&
(identical(other.expire, expire) || other.expire == expire));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, upload, download, total, expire);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$UserInfoImplCopyWith<_$UserInfoImpl> get copyWith =>
__$$UserInfoImplCopyWithImpl<_$UserInfoImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UserInfoImplToJson(
this,
);
}
}
abstract class _UserInfo implements UserInfo {
const factory _UserInfo(
{final int upload,
final int download,
final int total,
final int expire}) = _$UserInfoImpl;
factory _UserInfo.fromJson(Map<String, dynamic> json) =
_$UserInfoImpl.fromJson;
@override
int get upload;
@override
int get download;
@override
int get total;
@override
int get expire;
@override
@JsonKey(ignore: true)
_$$UserInfoImplCopyWith<_$UserInfoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
Profile _$ProfileFromJson(Map<String, dynamic> json) {
return _Profile.fromJson(json);
}
/// @nodoc
mixin _$Profile {
String get id => throw _privateConstructorUsedError;
String? get label => throw _privateConstructorUsedError;
String? get currentGroupName => throw _privateConstructorUsedError;
String get url => throw _privateConstructorUsedError;
DateTime? get lastUpdateDate => throw _privateConstructorUsedError;
Duration get autoUpdateDuration => throw _privateConstructorUsedError;
UserInfo? get userInfo => throw _privateConstructorUsedError;
bool get autoUpdate => throw _privateConstructorUsedError;
Map<String, String> get selectedMap => throw _privateConstructorUsedError;
Set<String> get unfoldSet => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ProfileCopyWith<Profile> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProfileCopyWith<$Res> {
factory $ProfileCopyWith(Profile value, $Res Function(Profile) then) =
_$ProfileCopyWithImpl<$Res, Profile>;
@useResult
$Res call(
{String id,
String? label,
String? currentGroupName,
String url,
DateTime? lastUpdateDate,
Duration autoUpdateDuration,
UserInfo? userInfo,
bool autoUpdate,
Map<String, String> selectedMap,
Set<String> unfoldSet});
$UserInfoCopyWith<$Res>? get userInfo;
}
/// @nodoc
class _$ProfileCopyWithImpl<$Res, $Val extends Profile>
implements $ProfileCopyWith<$Res> {
_$ProfileCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? label = freezed,
Object? currentGroupName = freezed,
Object? url = null,
Object? lastUpdateDate = freezed,
Object? autoUpdateDuration = null,
Object? userInfo = freezed,
Object? autoUpdate = null,
Object? selectedMap = null,
Object? unfoldSet = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
label: freezed == label
? _value.label
: label // ignore: cast_nullable_to_non_nullable
as String?,
currentGroupName: freezed == currentGroupName
? _value.currentGroupName
: currentGroupName // ignore: cast_nullable_to_non_nullable
as String?,
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
lastUpdateDate: freezed == lastUpdateDate
? _value.lastUpdateDate
: lastUpdateDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
autoUpdateDuration: null == autoUpdateDuration
? _value.autoUpdateDuration
: autoUpdateDuration // ignore: cast_nullable_to_non_nullable
as Duration,
userInfo: freezed == userInfo
? _value.userInfo
: userInfo // ignore: cast_nullable_to_non_nullable
as UserInfo?,
autoUpdate: null == autoUpdate
? _value.autoUpdate
: autoUpdate // ignore: cast_nullable_to_non_nullable
as bool,
selectedMap: null == selectedMap
? _value.selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
unfoldSet: null == unfoldSet
? _value.unfoldSet
: unfoldSet // ignore: cast_nullable_to_non_nullable
as Set<String>,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$UserInfoCopyWith<$Res>? get userInfo {
if (_value.userInfo == null) {
return null;
}
return $UserInfoCopyWith<$Res>(_value.userInfo!, (value) {
return _then(_value.copyWith(userInfo: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ProfileImplCopyWith<$Res> implements $ProfileCopyWith<$Res> {
factory _$$ProfileImplCopyWith(
_$ProfileImpl value, $Res Function(_$ProfileImpl) then) =
__$$ProfileImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String? label,
String? currentGroupName,
String url,
DateTime? lastUpdateDate,
Duration autoUpdateDuration,
UserInfo? userInfo,
bool autoUpdate,
Map<String, String> selectedMap,
Set<String> unfoldSet});
@override
$UserInfoCopyWith<$Res>? get userInfo;
}
/// @nodoc
class __$$ProfileImplCopyWithImpl<$Res>
extends _$ProfileCopyWithImpl<$Res, _$ProfileImpl>
implements _$$ProfileImplCopyWith<$Res> {
__$$ProfileImplCopyWithImpl(
_$ProfileImpl _value, $Res Function(_$ProfileImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? label = freezed,
Object? currentGroupName = freezed,
Object? url = null,
Object? lastUpdateDate = freezed,
Object? autoUpdateDuration = null,
Object? userInfo = freezed,
Object? autoUpdate = null,
Object? selectedMap = null,
Object? unfoldSet = null,
}) {
return _then(_$ProfileImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
label: freezed == label
? _value.label
: label // ignore: cast_nullable_to_non_nullable
as String?,
currentGroupName: freezed == currentGroupName
? _value.currentGroupName
: currentGroupName // ignore: cast_nullable_to_non_nullable
as String?,
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
lastUpdateDate: freezed == lastUpdateDate
? _value.lastUpdateDate
: lastUpdateDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
autoUpdateDuration: null == autoUpdateDuration
? _value.autoUpdateDuration
: autoUpdateDuration // ignore: cast_nullable_to_non_nullable
as Duration,
userInfo: freezed == userInfo
? _value.userInfo
: userInfo // ignore: cast_nullable_to_non_nullable
as UserInfo?,
autoUpdate: null == autoUpdate
? _value.autoUpdate
: autoUpdate // ignore: cast_nullable_to_non_nullable
as bool,
selectedMap: null == selectedMap
? _value._selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
unfoldSet: null == unfoldSet
? _value._unfoldSet
: unfoldSet // ignore: cast_nullable_to_non_nullable
as Set<String>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ProfileImpl implements _Profile {
const _$ProfileImpl(
{required this.id,
this.label,
this.currentGroupName,
this.url = "",
this.lastUpdateDate,
required this.autoUpdateDuration,
this.userInfo,
this.autoUpdate = true,
final Map<String, String> selectedMap = const {},
final Set<String> unfoldSet = const {}})
: _selectedMap = selectedMap,
_unfoldSet = unfoldSet;
factory _$ProfileImpl.fromJson(Map<String, dynamic> json) =>
_$$ProfileImplFromJson(json);
@override
final String id;
@override
final String? label;
@override
final String? currentGroupName;
@override
@JsonKey()
final String url;
@override
final DateTime? lastUpdateDate;
@override
final Duration autoUpdateDuration;
@override
final UserInfo? userInfo;
@override
@JsonKey()
final bool autoUpdate;
final Map<String, String> _selectedMap;
@override
@JsonKey()
Map<String, String> get selectedMap {
if (_selectedMap is EqualUnmodifiableMapView) return _selectedMap;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_selectedMap);
}
final Set<String> _unfoldSet;
@override
@JsonKey()
Set<String> get unfoldSet {
if (_unfoldSet is EqualUnmodifiableSetView) return _unfoldSet;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_unfoldSet);
}
@override
String toString() {
return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProfileImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.label, label) || other.label == label) &&
(identical(other.currentGroupName, currentGroupName) ||
other.currentGroupName == currentGroupName) &&
(identical(other.url, url) || other.url == url) &&
(identical(other.lastUpdateDate, lastUpdateDate) ||
other.lastUpdateDate == lastUpdateDate) &&
(identical(other.autoUpdateDuration, autoUpdateDuration) ||
other.autoUpdateDuration == autoUpdateDuration) &&
(identical(other.userInfo, userInfo) ||
other.userInfo == userInfo) &&
(identical(other.autoUpdate, autoUpdate) ||
other.autoUpdate == autoUpdate) &&
const DeepCollectionEquality()
.equals(other._selectedMap, _selectedMap) &&
const DeepCollectionEquality()
.equals(other._unfoldSet, _unfoldSet));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
id,
label,
currentGroupName,
url,
lastUpdateDate,
autoUpdateDuration,
userInfo,
autoUpdate,
const DeepCollectionEquality().hash(_selectedMap),
const DeepCollectionEquality().hash(_unfoldSet));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ProfileImplCopyWith<_$ProfileImpl> get copyWith =>
__$$ProfileImplCopyWithImpl<_$ProfileImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ProfileImplToJson(
this,
);
}
}
abstract class _Profile implements Profile {
const factory _Profile(
{required final String id,
final String? label,
final String? currentGroupName,
final String url,
final DateTime? lastUpdateDate,
required final Duration autoUpdateDuration,
final UserInfo? userInfo,
final bool autoUpdate,
final Map<String, String> selectedMap,
final Set<String> unfoldSet}) = _$ProfileImpl;
factory _Profile.fromJson(Map<String, dynamic> json) = _$ProfileImpl.fromJson;
@override
String get id;
@override
String? get label;
@override
String? get currentGroupName;
@override
String get url;
@override
DateTime? get lastUpdateDate;
@override
Duration get autoUpdateDuration;
@override
UserInfo? get userInfo;
@override
bool get autoUpdate;
@override
Map<String, String> get selectedMap;
@override
Set<String> get unfoldSet;
@override
@JsonKey(ignore: true)
_$$ProfileImplCopyWith<_$ProfileImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -6,41 +6,49 @@ part of '../profile.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
UserInfo _$UserInfoFromJson(Map<String, dynamic> json) => UserInfo( _$UserInfoImpl _$$UserInfoImplFromJson(Map<String, dynamic> json) =>
upload: (json['upload'] as num?)?.toInt(), _$UserInfoImpl(
download: (json['download'] as num?)?.toInt(), upload: (json['upload'] as num?)?.toInt() ?? 0,
total: (json['total'] as num?)?.toInt(), download: (json['download'] as num?)?.toInt() ?? 0,
expire: (json['expire'] as num?)?.toInt(), total: (json['total'] as num?)?.toInt() ?? 0,
expire: (json['expire'] as num?)?.toInt() ?? 0,
); );
Map<String, dynamic> _$UserInfoToJson(UserInfo instance) => <String, dynamic>{ Map<String, dynamic> _$$UserInfoImplToJson(_$UserInfoImpl instance) =>
<String, dynamic>{
'upload': instance.upload, 'upload': instance.upload,
'download': instance.download, 'download': instance.download,
'total': instance.total, 'total': instance.total,
'expire': instance.expire, 'expire': instance.expire,
}; };
Profile _$ProfileFromJson(Map<String, dynamic> json) => Profile( _$ProfileImpl _$$ProfileImplFromJson(Map<String, dynamic> json) =>
id: json['id'] as String?, _$ProfileImpl(
id: json['id'] as String,
label: json['label'] as String?, label: json['label'] as String?,
url: json['url'] as String?,
currentGroupName: json['currentGroupName'] as String?, currentGroupName: json['currentGroupName'] as String?,
userInfo: json['userInfo'] == null url: json['url'] as String? ?? "",
? null
: UserInfo.fromJson(json['userInfo'] as Map<String, dynamic>),
lastUpdateDate: json['lastUpdateDate'] == null lastUpdateDate: json['lastUpdateDate'] == null
? null ? null
: DateTime.parse(json['lastUpdateDate'] as String), : DateTime.parse(json['lastUpdateDate'] as String),
selectedMap: (json['selectedMap'] as Map<String, dynamic>?)?.map( autoUpdateDuration:
(k, e) => MapEntry(k, e as String), Duration(microseconds: (json['autoUpdateDuration'] as num).toInt()),
), userInfo: json['userInfo'] == null
autoUpdateDuration: json['autoUpdateDuration'] == null
? null ? null
: Duration(microseconds: (json['autoUpdateDuration'] as num).toInt()), : UserInfo.fromJson(json['userInfo'] as Map<String, dynamic>),
autoUpdate: json['autoUpdate'] as bool? ?? true, autoUpdate: json['autoUpdate'] as bool? ?? true,
selectedMap: (json['selectedMap'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
) ??
const {},
unfoldSet: (json['unfoldSet'] as List<dynamic>?)
?.map((e) => e as String)
.toSet() ??
const {},
); );
Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{ Map<String, dynamic> _$$ProfileImplToJson(_$ProfileImpl instance) =>
<String, dynamic>{
'id': instance.id, 'id': instance.id,
'label': instance.label, 'label': instance.label,
'currentGroupName': instance.currentGroupName, 'currentGroupName': instance.currentGroupName,
@@ -50,4 +58,5 @@ Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{
'userInfo': instance.userInfo, 'userInfo': instance.userInfo,
'autoUpdate': instance.autoUpdate, 'autoUpdate': instance.autoUpdate,
'selectedMap': instance.selectedMap, 'selectedMap': instance.selectedMap,
'unfoldSet': instance.unfoldSet.toList(),
}; };

View File

@@ -1730,39 +1730,37 @@ abstract class _ProxiesSelectorState implements ProxiesSelectorState {
} }
/// @nodoc /// @nodoc
mixin _$ProxiesTabViewSelectorState { mixin _$ProxyGroupSelectorState {
ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError; ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError;
ProxyCardType get proxyCardType => throw _privateConstructorUsedError;
num get sortNum => throw _privateConstructorUsedError; num get sortNum => throw _privateConstructorUsedError;
Group get group => throw _privateConstructorUsedError; List<Proxy> get proxies => throw _privateConstructorUsedError;
ViewMode get viewMode => throw _privateConstructorUsedError; int get columns => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
$ProxiesTabViewSelectorStateCopyWith<ProxiesTabViewSelectorState> $ProxyGroupSelectorStateCopyWith<ProxyGroupSelectorState> get copyWith =>
get copyWith => throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
/// @nodoc /// @nodoc
abstract class $ProxiesTabViewSelectorStateCopyWith<$Res> { abstract class $ProxyGroupSelectorStateCopyWith<$Res> {
factory $ProxiesTabViewSelectorStateCopyWith( factory $ProxyGroupSelectorStateCopyWith(ProxyGroupSelectorState value,
ProxiesTabViewSelectorState value, $Res Function(ProxyGroupSelectorState) then) =
$Res Function(ProxiesTabViewSelectorState) then) = _$ProxyGroupSelectorStateCopyWithImpl<$Res, ProxyGroupSelectorState>;
_$ProxiesTabViewSelectorStateCopyWithImpl<$Res,
ProxiesTabViewSelectorState>;
@useResult @useResult
$Res call( $Res call(
{ProxiesSortType proxiesSortType, {ProxiesSortType proxiesSortType,
ProxyCardType proxyCardType,
num sortNum, num sortNum,
Group group, List<Proxy> proxies,
ViewMode viewMode}); int columns});
$GroupCopyWith<$Res> get group;
} }
/// @nodoc /// @nodoc
class _$ProxiesTabViewSelectorStateCopyWithImpl<$Res, class _$ProxyGroupSelectorStateCopyWithImpl<$Res,
$Val extends ProxiesTabViewSelectorState> $Val extends ProxyGroupSelectorState>
implements $ProxiesTabViewSelectorStateCopyWith<$Res> { implements $ProxyGroupSelectorStateCopyWith<$Res> {
_$ProxiesTabViewSelectorStateCopyWithImpl(this._value, this._then); _$ProxyGroupSelectorStateCopyWithImpl(this._value, this._then);
// ignore: unused_field // ignore: unused_field
final $Val _value; final $Val _value;
@@ -1773,165 +1771,177 @@ class _$ProxiesTabViewSelectorStateCopyWithImpl<$Res,
@override @override
$Res call({ $Res call({
Object? proxiesSortType = null, Object? proxiesSortType = null,
Object? proxyCardType = null,
Object? sortNum = null, Object? sortNum = null,
Object? group = null, Object? proxies = null,
Object? viewMode = null, Object? columns = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
proxiesSortType: null == proxiesSortType proxiesSortType: null == proxiesSortType
? _value.proxiesSortType ? _value.proxiesSortType
: proxiesSortType // ignore: cast_nullable_to_non_nullable : proxiesSortType // ignore: cast_nullable_to_non_nullable
as ProxiesSortType, as ProxiesSortType,
proxyCardType: null == proxyCardType
? _value.proxyCardType
: proxyCardType // ignore: cast_nullable_to_non_nullable
as ProxyCardType,
sortNum: null == sortNum sortNum: null == sortNum
? _value.sortNum ? _value.sortNum
: sortNum // ignore: cast_nullable_to_non_nullable : sortNum // ignore: cast_nullable_to_non_nullable
as num, as num,
group: null == group proxies: null == proxies
? _value.group ? _value.proxies
: group // ignore: cast_nullable_to_non_nullable : proxies // ignore: cast_nullable_to_non_nullable
as Group, as List<Proxy>,
viewMode: null == viewMode columns: null == columns
? _value.viewMode ? _value.columns
: viewMode // ignore: cast_nullable_to_non_nullable : columns // ignore: cast_nullable_to_non_nullable
as ViewMode, as int,
) as $Val); ) as $Val);
} }
@override
@pragma('vm:prefer-inline')
$GroupCopyWith<$Res> get group {
return $GroupCopyWith<$Res>(_value.group, (value) {
return _then(_value.copyWith(group: value) as $Val);
});
}
} }
/// @nodoc /// @nodoc
abstract class _$$ProxiesTabViewSelectorStateImplCopyWith<$Res> abstract class _$$ProxyGroupSelectorStateImplCopyWith<$Res>
implements $ProxiesTabViewSelectorStateCopyWith<$Res> { implements $ProxyGroupSelectorStateCopyWith<$Res> {
factory _$$ProxiesTabViewSelectorStateImplCopyWith( factory _$$ProxyGroupSelectorStateImplCopyWith(
_$ProxiesTabViewSelectorStateImpl value, _$ProxyGroupSelectorStateImpl value,
$Res Function(_$ProxiesTabViewSelectorStateImpl) then) = $Res Function(_$ProxyGroupSelectorStateImpl) then) =
__$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res>; __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call( $Res call(
{ProxiesSortType proxiesSortType, {ProxiesSortType proxiesSortType,
ProxyCardType proxyCardType,
num sortNum, num sortNum,
Group group, List<Proxy> proxies,
ViewMode viewMode}); int columns});
@override
$GroupCopyWith<$Res> get group;
} }
/// @nodoc /// @nodoc
class __$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res> class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>
extends _$ProxiesTabViewSelectorStateCopyWithImpl<$Res, extends _$ProxyGroupSelectorStateCopyWithImpl<$Res,
_$ProxiesTabViewSelectorStateImpl> _$ProxyGroupSelectorStateImpl>
implements _$$ProxiesTabViewSelectorStateImplCopyWith<$Res> { implements _$$ProxyGroupSelectorStateImplCopyWith<$Res> {
__$$ProxiesTabViewSelectorStateImplCopyWithImpl( __$$ProxyGroupSelectorStateImplCopyWithImpl(
_$ProxiesTabViewSelectorStateImpl _value, _$ProxyGroupSelectorStateImpl _value,
$Res Function(_$ProxiesTabViewSelectorStateImpl) _then) $Res Function(_$ProxyGroupSelectorStateImpl) _then)
: super(_value, _then); : super(_value, _then);
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? proxiesSortType = null, Object? proxiesSortType = null,
Object? proxyCardType = null,
Object? sortNum = null, Object? sortNum = null,
Object? group = null, Object? proxies = null,
Object? viewMode = null, Object? columns = null,
}) { }) {
return _then(_$ProxiesTabViewSelectorStateImpl( return _then(_$ProxyGroupSelectorStateImpl(
proxiesSortType: null == proxiesSortType proxiesSortType: null == proxiesSortType
? _value.proxiesSortType ? _value.proxiesSortType
: proxiesSortType // ignore: cast_nullable_to_non_nullable : proxiesSortType // ignore: cast_nullable_to_non_nullable
as ProxiesSortType, as ProxiesSortType,
proxyCardType: null == proxyCardType
? _value.proxyCardType
: proxyCardType // ignore: cast_nullable_to_non_nullable
as ProxyCardType,
sortNum: null == sortNum sortNum: null == sortNum
? _value.sortNum ? _value.sortNum
: sortNum // ignore: cast_nullable_to_non_nullable : sortNum // ignore: cast_nullable_to_non_nullable
as num, as num,
group: null == group proxies: null == proxies
? _value.group ? _value._proxies
: group // ignore: cast_nullable_to_non_nullable : proxies // ignore: cast_nullable_to_non_nullable
as Group, as List<Proxy>,
viewMode: null == viewMode columns: null == columns
? _value.viewMode ? _value.columns
: viewMode // ignore: cast_nullable_to_non_nullable : columns // ignore: cast_nullable_to_non_nullable
as ViewMode, as int,
)); ));
} }
} }
/// @nodoc /// @nodoc
class _$ProxiesTabViewSelectorStateImpl class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
implements _ProxiesTabViewSelectorState { const _$ProxyGroupSelectorStateImpl(
const _$ProxiesTabViewSelectorStateImpl(
{required this.proxiesSortType, {required this.proxiesSortType,
required this.proxyCardType,
required this.sortNum, required this.sortNum,
required this.group, required final List<Proxy> proxies,
required this.viewMode}); required this.columns})
: _proxies = proxies;
@override @override
final ProxiesSortType proxiesSortType; final ProxiesSortType proxiesSortType;
@override @override
final ProxyCardType proxyCardType;
@override
final num sortNum; final num sortNum;
final List<Proxy> _proxies;
@override @override
final Group group; List<Proxy> get proxies {
if (_proxies is EqualUnmodifiableListView) return _proxies;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_proxies);
}
@override @override
final ViewMode viewMode; final int columns;
@override @override
String toString() { String toString() {
return 'ProxiesTabViewSelectorState(proxiesSortType: $proxiesSortType, sortNum: $sortNum, group: $group, viewMode: $viewMode)'; return 'ProxyGroupSelectorState(proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, proxies: $proxies, columns: $columns)';
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$ProxiesTabViewSelectorStateImpl && other is _$ProxyGroupSelectorStateImpl &&
(identical(other.proxiesSortType, proxiesSortType) || (identical(other.proxiesSortType, proxiesSortType) ||
other.proxiesSortType == proxiesSortType) && other.proxiesSortType == proxiesSortType) &&
(identical(other.proxyCardType, proxyCardType) ||
other.proxyCardType == proxyCardType) &&
(identical(other.sortNum, sortNum) || other.sortNum == sortNum) && (identical(other.sortNum, sortNum) || other.sortNum == sortNum) &&
(identical(other.group, group) || other.group == group) && const DeepCollectionEquality().equals(other._proxies, _proxies) &&
(identical(other.viewMode, viewMode) || (identical(other.columns, columns) || other.columns == columns));
other.viewMode == viewMode));
} }
@override @override
int get hashCode => int get hashCode => Object.hash(runtimeType, proxiesSortType, proxyCardType,
Object.hash(runtimeType, proxiesSortType, sortNum, group, viewMode); sortNum, const DeepCollectionEquality().hash(_proxies), columns);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$ProxiesTabViewSelectorStateImplCopyWith<_$ProxiesTabViewSelectorStateImpl> _$$ProxyGroupSelectorStateImplCopyWith<_$ProxyGroupSelectorStateImpl>
get copyWith => __$$ProxiesTabViewSelectorStateImplCopyWithImpl< get copyWith => __$$ProxyGroupSelectorStateImplCopyWithImpl<
_$ProxiesTabViewSelectorStateImpl>(this, _$identity); _$ProxyGroupSelectorStateImpl>(this, _$identity);
} }
abstract class _ProxiesTabViewSelectorState abstract class _ProxyGroupSelectorState implements ProxyGroupSelectorState {
implements ProxiesTabViewSelectorState { const factory _ProxyGroupSelectorState(
const factory _ProxiesTabViewSelectorState(
{required final ProxiesSortType proxiesSortType, {required final ProxiesSortType proxiesSortType,
required final ProxyCardType proxyCardType,
required final num sortNum, required final num sortNum,
required final Group group, required final List<Proxy> proxies,
required final ViewMode viewMode}) = _$ProxiesTabViewSelectorStateImpl; required final int columns}) = _$ProxyGroupSelectorStateImpl;
@override @override
ProxiesSortType get proxiesSortType; ProxiesSortType get proxiesSortType;
@override @override
ProxyCardType get proxyCardType;
@override
num get sortNum; num get sortNum;
@override @override
Group get group; List<Proxy> get proxies;
@override @override
ViewMode get viewMode; int get columns;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$ProxiesTabViewSelectorStateImplCopyWith<_$ProxiesTabViewSelectorStateImpl> _$$ProxyGroupSelectorStateImplCopyWith<_$ProxyGroupSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }

View File

@@ -1,8 +1,12 @@
// ignore_for_file: invalid_annotation_target
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/log.g.dart'; part 'generated/log.g.dart';
part 'generated/log.freezed.dart';
@JsonSerializable() @JsonSerializable()
class Log { class Log {
@JsonKey(name: "LogLevel") @JsonKey(name: "LogLevel")
@@ -31,3 +35,22 @@ class Log {
return 'Log{logLevel: $logLevel, payload: $payload, dateTime: $dateTime}'; return 'Log{logLevel: $logLevel, payload: $payload, dateTime: $dateTime}';
} }
} }
@freezed
class LogsAndKeywords with _$LogsAndKeywords {
const factory LogsAndKeywords({
@Default([]) List<Log> logs,
@Default([]) List<String> keywords,
}) = _LogsAndKeywords;
factory LogsAndKeywords.fromJson(Map<String, Object?> json) =>
_$LogsAndKeywordsFromJson(json);
}
extension LogsAndKeywordsExt on LogsAndKeywords {
List<Log> get filteredLogs => logs
.where(
(log) => {log.logLevel.name}.containsAll(keywords),
)
.toList();
}

View File

@@ -5,39 +5,28 @@ import 'dart:typed_data';
import 'package:fl_clash/clash/core.dart'; import 'package:fl_clash/clash/core.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/profile.g.dart'; part 'generated/profile.g.dart';
part 'generated/profile.freezed.dart';
typedef SelectedMap = Map<String, String>; typedef SelectedMap = Map<String, String>;
@JsonSerializable() @freezed
class UserInfo { class UserInfo with _$UserInfo {
int upload; const factory UserInfo({
int download; @Default(0) int upload,
int total; @Default(0) int download,
int expire; @Default(0) int total,
@Default(0) int expire,
}) = _UserInfo;
UserInfo({ factory UserInfo.fromJson(Map<String, Object?> json) =>
int? upload, _$UserInfoFromJson(json);
int? download,
int? total,
int? expire,
}) : upload = upload ?? 0,
download = download ?? 0,
total = total ?? 0,
expire = expire ?? 0;
Map<String, dynamic> toJson() {
return _$UserInfoToJson(this);
}
factory UserInfo.fromJson(Map<String, dynamic> json) {
return _$UserInfoFromJson(json);
}
factory UserInfo.formHString(String? info) { factory UserInfo.formHString(String? info) {
if (info == null) return UserInfo(); if (info == null) return const UserInfo();
final list = info.split(";"); final list = info.split(";");
Map<String, int?> map = {}; Map<String, int?> map = {};
for (final i in list) { for (final i in list) {
@@ -45,73 +34,77 @@ class UserInfo {
map[keyValue[0]] = int.tryParse(keyValue[1]); map[keyValue[0]] = int.tryParse(keyValue[1]);
} }
return UserInfo( return UserInfo(
upload: map["upload"], upload: map["upload"] ?? 0,
download: map["download"], download: map["download"] ?? 0,
total: map["total"], total: map["total"] ?? 0,
expire: map["expire"], expire: map["expire"] ?? 0,
); );
} }
@override
String toString() {
return 'UserInfo{upload: $upload, download: $download, total: $total, expire: $expire}';
}
} }
@JsonSerializable() @freezed
class Profile { class Profile with _$Profile {
String id; const factory Profile({
String? label; required String id,
String? currentGroupName; String? label,
String? url; String? currentGroupName,
DateTime? lastUpdateDate; @Default("") String url,
Duration autoUpdateDuration; DateTime? lastUpdateDate,
UserInfo? userInfo; required Duration autoUpdateDuration,
bool autoUpdate; UserInfo? userInfo,
SelectedMap selectedMap; @Default(true) bool autoUpdate,
@Default({}) SelectedMap selectedMap,
@Default({}) Set<String> unfoldSet,
}) = _Profile;
Profile({ factory Profile.fromJson(Map<String, Object?> json) =>
String? id, _$ProfileFromJson(json);
this.label,
this.url,
this.currentGroupName,
this.userInfo,
this.lastUpdateDate,
SelectedMap? selectedMap,
Duration? autoUpdateDuration,
this.autoUpdate = true,
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
autoUpdateDuration = autoUpdateDuration ?? defaultUpdateDuration,
selectedMap = selectedMap ?? {};
factory Profile.normal({
String? label,
String url = '',
}) {
return Profile(
label: label,
url: url,
id: DateTime.now().millisecondsSinceEpoch.toString(),
autoUpdateDuration: defaultUpdateDuration,
);
}
}
extension ProfileExtension on Profile {
ProfileType get type => ProfileType get type =>
url == null || url?.isEmpty == true ? ProfileType.file : ProfileType.url; url.isEmpty == true ? ProfileType.file : ProfileType.url;
bool get realAutoUpdate =>
url.isEmpty == true ? false : autoUpdate;
Future<void> checkAndUpdate() async { Future<void> checkAndUpdate() async {
final isExists = await check(); final isExists = await check();
if (!isExists) { if (!isExists) {
if (url != null) { if (url.isNotEmpty) {
return await update(); await update();
} }
} }
} }
Future<void> update() async {
final response = await request.getFileResponseForUrl(url!);
final disposition = response.headers.value("content-disposition");
label ??= other.getFileNameForDisposition(disposition) ?? id;
final userinfo = response.headers.value('subscription-userinfo');
userInfo = UserInfo.formHString(userinfo);
await saveFile(response.data);
lastUpdateDate = DateTime.now();
}
Future<bool> check() async { Future<bool> check() async {
final profilePath = await appPath.getProfilePath(id); final profilePath = await appPath.getProfilePath(id);
return await File(profilePath!).exists(); return await File(profilePath!).exists();
} }
Future<void> saveFile(Uint8List bytes) async { Future<Profile> update() async {
final response = await request.getFileResponseForUrl(url);
final disposition = response.headers.value("content-disposition");
final userinfo = response.headers.value('subscription-userinfo');
return await copyWith(
label: label ?? other.getFileNameForDisposition(disposition) ?? id,
userInfo: UserInfo.formHString(userinfo),
).saveFile(response.data);
}
Future<Profile> saveFile(Uint8List bytes) async {
final message = await clashCore.validateConfig(utf8.decode(bytes)); final message = await clashCore.validateConfig(utf8.decode(bytes));
if (message.isNotEmpty) { if (message.isNotEmpty) {
throw message; throw message;
@@ -123,66 +116,21 @@ class Profile {
await file.create(recursive: true); await file.create(recursive: true);
} }
await file.writeAsBytes(bytes); await file.writeAsBytes(bytes);
lastUpdateDate = DateTime.now(); return copyWith(lastUpdateDate: DateTime.now());
} }
Map<String, dynamic> toJson() { Future<Profile> saveFileWithString(String value) async {
return _$ProfileToJson(this); final message = await clashCore.validateConfig(value);
} if (message.isNotEmpty) {
throw message;
factory Profile.fromJson(Map<String, dynamic> json) { }
return _$ProfileFromJson(json); final path = await appPath.getProfilePath(id);
} final file = File(path!);
final isExists = await file.exists();
if (!isExists) {
@override await file.create(recursive: true);
bool operator ==(Object other) => }
identical(this, other) || await file.writeAsString(value);
other is Profile && return copyWith(lastUpdateDate: DateTime.now());
runtimeType == other.runtimeType &&
id == other.id &&
label == other.label &&
currentGroupName == other.currentGroupName &&
url == other.url &&
lastUpdateDate == other.lastUpdateDate &&
autoUpdateDuration == other.autoUpdateDuration &&
userInfo == other.userInfo &&
autoUpdate == other.autoUpdate &&
selectedMap == other.selectedMap;
@override
int get hashCode =>
id.hashCode ^
label.hashCode ^
currentGroupName.hashCode ^
url.hashCode ^
lastUpdateDate.hashCode ^
autoUpdateDuration.hashCode ^
userInfo.hashCode ^
autoUpdate.hashCode ^
selectedMap.hashCode;
Profile copyWith({
String? label,
String? url,
UserInfo? userInfo,
String? currentGroupName,
String? proxyName,
DateTime? lastUpdateDate,
Duration? autoUpdateDuration,
bool? autoUpdate,
SelectedMap? selectedMap,
}) {
return Profile(
id: id,
label: label ?? this.label,
url: url ?? this.url,
currentGroupName: currentGroupName ?? this.currentGroupName,
userInfo: userInfo ?? this.userInfo,
selectedMap: selectedMap ?? this.selectedMap,
lastUpdateDate: lastUpdateDate ?? this.lastUpdateDate,
autoUpdateDuration: autoUpdateDuration ?? this.autoUpdateDuration,
autoUpdate: autoUpdate ?? this.autoUpdate,
);
} }
} }

View File

@@ -99,13 +99,14 @@ class ProxiesSelectorState with _$ProxiesSelectorState {
} }
@freezed @freezed
class ProxiesTabViewSelectorState with _$ProxiesTabViewSelectorState { class ProxyGroupSelectorState with _$ProxyGroupSelectorState {
const factory ProxiesTabViewSelectorState({ const factory ProxyGroupSelectorState({
required ProxiesSortType proxiesSortType, required ProxiesSortType proxiesSortType,
required ProxyCardType proxyCardType,
required num sortNum, required num sortNum,
required Group group, required List<Proxy> proxies,
required ViewMode viewMode, required int columns,
}) = _ProxiesTabViewSelectorState; }) = _ProxyGroupSelectorState;
} }
@freezed @freezed

View File

@@ -56,6 +56,10 @@ class Proxy extends ProxyPlatform {
return await methodChannel.invokeMethod<bool?>("SetProtect", {'fd': fd}); return await methodChannel.invokeMethod<bool?>("SetProtect", {'fd': fd});
} }
Future<int?> getRunTimeStamp() async {
return await methodChannel.invokeMethod<int?>("GetRunTimeStamp");
}
Future<bool?> startForeground({ Future<bool?> startForeground({
required String title, required String title,
required String content, required String content,
@@ -75,8 +79,19 @@ class Proxy extends ProxyPlatform {
} }
} }
// updateStartTime() async {
// startTime = clashCore.getRunTime();
// }
updateStartTime() async { updateStartTime() async {
startTime = clashCore.getRunTime(); startTime = await getRunTime();
}
Future<DateTime?> getRunTime() async {
final runTimeStamp = await getRunTimeStamp();
return runTimeStamp != null
? DateTime.fromMillisecondsSinceEpoch(runTimeStamp)
: null;
} }
} }

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/widgets/scaffold.dart'; import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -57,6 +58,7 @@ class GlobalState {
} }
Future<void> startSystemProxy({ Future<void> startSystemProxy({
required AppState appState,
required Config config, required Config config,
required ClashConfig clashConfig, required ClashConfig clashConfig,
}) async { }) async {
@@ -73,6 +75,11 @@ class GlobalState {
args: args, args: args,
); );
startListenUpdate(); startListenUpdate();
applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
} }
Future<void> stopSystemProxy() async { Future<void> stopSystemProxy() async {
@@ -195,6 +202,7 @@ class GlobalState {
final traffic = clashCore.getTraffic(); final traffic = clashCore.getTraffic();
if (appState != null) { if (appState != null) {
appState.addTraffic(traffic); appState.addTraffic(traffic);
appState.totalTraffic = clashCore.getTotalTraffic();
} }
if (Platform.isAndroid) { if (Platform.isAndroid) {
final currentProfile = config.currentProfile; final currentProfile = config.currentProfile;
@@ -254,6 +262,18 @@ class GlobalState {
return null; return null;
} }
} }
int getColumns(ViewMode viewMode,int currentColumns){
final targetColumnsArray = switch (viewMode) {
ViewMode.mobile => [2, 1],
ViewMode.laptop => [3, 2],
ViewMode.desktop => [4, 3],
};
if (targetColumnsArray.contains(currentColumns)) {
return currentColumns;
}
return targetColumnsArray.first;
}
} }
final globalState = GlobalState(); final globalState = GlobalState();

View File

@@ -1,4 +1,5 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'text.dart'; import 'text.dart';
@@ -54,6 +55,7 @@ class CommonCard extends StatelessWidget {
const CommonCard({ const CommonCard({
super.key, super.key,
bool? isSelected, bool? isSelected,
this.type = CommonCardType.plain,
this.onPressed, this.onPressed,
this.info, this.info,
this.selectWidget, this.selectWidget,
@@ -65,10 +67,14 @@ class CommonCard extends StatelessWidget {
final Widget? selectWidget; final Widget? selectWidget;
final Widget child; final Widget child;
final Info? info; final Info? info;
final CommonCardType type;
BorderSide getBorderSide(BuildContext context, Set<WidgetState> states) { BorderSide getBorderSide(BuildContext context, Set<WidgetState> states) {
if(type == CommonCardType.filled){
return BorderSide.none;
}
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
var hoverColor = isSelected final hoverColor = isSelected
? colorScheme.primary.toLight() ? colorScheme.primary.toLight()
: colorScheme.primary.toLighter(); : colorScheme.primary.toLighter();
if (states.contains(WidgetState.hovered) || if (states.contains(WidgetState.hovered) ||
@@ -86,17 +92,28 @@ class CommonCard extends StatelessWidget {
Color? getBackgroundColor(BuildContext context, Set<WidgetState> states) { Color? getBackgroundColor(BuildContext context, Set<WidgetState> states) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
if (isSelected) { switch(type){
return colorScheme.secondaryContainer; case CommonCardType.plain:
if (isSelected) {
return colorScheme.secondaryContainer;
}
if (states.isEmpty) {
return colorScheme.secondaryContainer.toLittle();
}
return Theme.of(context)
.outlinedButtonTheme
.style
?.backgroundColor
?.resolve(states);
case CommonCardType.filled:
if (isSelected) {
return colorScheme.secondaryContainer;
}
if (states.isEmpty) {
return colorScheme.surfaceContainerLow;
}
return colorScheme.surfaceContainer;
} }
if (states.isEmpty) {
return colorScheme.secondaryContainer.toLittle();
}
return Theme.of(context)
.outlinedButtonTheme
.style
?.backgroundColor
?.resolve(states);
} }
@override @override
@@ -136,11 +153,7 @@ class CommonCard extends StatelessWidget {
(states) => getBorderSide(context, states), (states) => getBorderSide(context, states),
), ),
), ),
onPressed: () { onPressed: onPressed,
if (onPressed != null) {
onPressed!();
}
},
child: Builder( child: Builder(
builder: (_) { builder: (_) {
List<Widget> children = []; List<Widget> children = [];

View File

@@ -1,21 +1,45 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CommonChip extends StatelessWidget { class CommonChip extends StatelessWidget {
final String label; final String label;
final VoidCallback? onPressed;
final ChipType type;
final Widget? avatar;
const CommonChip({ const CommonChip({
super.key, super.key,
required this.label, required this.label,
this.onPressed,
this.avatar,
this.type = ChipType.action,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Chip( if (type == ChipType.delete) {
return Chip(
avatar: avatar,
padding: const EdgeInsets.symmetric(
vertical: 0,
horizontal: 4,
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onDeleted: onPressed ?? () {},
side:
BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)),
labelStyle: Theme.of(context).textTheme.bodyMedium,
label: Text(label),
);
}
return ActionChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
avatar: avatar,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 0, vertical: 0,
horizontal: 4, horizontal: 4,
), ),
onPressed: onPressed ?? () {},
side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)), side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)),
labelStyle: Theme.of(context).textTheme.bodyMedium, labelStyle: Theme.of(context).textTheme.bodyMedium,
label: Text(label), label: Text(label),

View File

@@ -1,6 +1,7 @@
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
@@ -61,7 +62,7 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
clashCore.setProcessMap( clashCore.setProcessMap(
ProcessMapItem( ProcessMapItem(
id: process.id, id: process.id,
value: packageName, value: packageName ?? "",
), ),
); );
super.onProcess(process); super.onProcess(process);
@@ -72,4 +73,10 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
globalState.appController.appState.addRequest(connection); globalState.appController.appState.addRequest(connection);
super.onRequest(connection); super.onRequest(connection);
} }
@override
void onRun(String runTime) async {
// proxy?.updateStartTime();
super.onRun(runTime);
}
} }

View File

@@ -11,7 +11,7 @@ class NullStatus extends StatelessWidget {
return Center( return Center(
child: Text( child: Text(
label, label,
style: Theme.of(context).textTheme.titleMedium?.toBold(), style: Theme.of(context).textTheme.titleMedium?.toBold,
), ),
); );
} }

View File

@@ -53,6 +53,12 @@ class _CommonPopupMenuState<T> extends State<CommonPopupMenu<T>> {
widget.onSelected(value); widget.onSelected(value);
} }
@override
void dispose() {
groupValue.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopupMenuButton<T>( return PopupMenuButton<T>(

View File

@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
class CommonScaffold extends StatefulWidget { class CommonScaffold extends StatefulWidget {
final Widget body; final Widget body;
final Widget? bottomNavigationBar; final Widget? bottomNavigationBar;
final Widget? floatingActionButton;
final String title; final String title;
final Widget? leading; final Widget? leading;
final List<Widget>? actions; final List<Widget>? actions;
@@ -19,6 +20,7 @@ class CommonScaffold extends StatefulWidget {
this.leading, this.leading,
required this.title, required this.title,
this.actions, this.actions,
this.floatingActionButton,
this.automaticallyImplyLeading = true, this.automaticallyImplyLeading = true,
}); });
@@ -86,6 +88,13 @@ class CommonScaffoldState extends State<CommonScaffold> {
} }
} }
@override
void dispose() {
_actions.dispose();
_floatingActionButton.dispose();
super.dispose();
}
@override @override
void didUpdateWidget(covariant CommonScaffold oldWidget) { void didUpdateWidget(covariant CommonScaffold oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
@@ -116,12 +125,13 @@ class CommonScaffoldState extends State<CommonScaffold> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _platformContainer( return _platformContainer(
child: Scaffold( child: Scaffold(
floatingActionButton: ValueListenableBuilder( floatingActionButton: widget.floatingActionButton ??
valueListenable: _floatingActionButton, ValueListenableBuilder(
builder: (_, floatingActionButton, __) { valueListenable: _floatingActionButton,
return floatingActionButton ?? Container(); builder: (_, floatingActionButton, __) {
}, return floatingActionButton ?? Container();
), },
),
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight), preferredSize: const Size.fromHeight(kToolbarHeight),

View File

@@ -517,6 +517,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
isolate_contactor:
dependency: transitive
description:
name: isolate_contactor
sha256: f1be0a90f91e4309ef37cc45280b2a84e769e848aae378318dd3dd263cfc482a
url: "https://pub.dev"
source: hosted
version: "4.2.0"
isolate_manager:
dependency: transitive
description:
name: isolate_manager
sha256: "8fb916c4444fd408f089448f904f083ac3e169ea1789fd4d987b25809af92188"
url: "https://pub.dev"
source: hosted
version: "4.3.1"
jovial_misc: jovial_misc:
dependency: transitive dependency: transitive
description: description:
@@ -705,10 +721,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.5" version: "2.2.6"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
@@ -820,6 +836,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" version: "3.2.1"
re_editor:
dependency: "direct main"
description:
name: re_editor
sha256: db7a82e95f0f74301e85d4d5c805a8b8a5ba43d6c0d26673b7e35dc011f06635
url: "https://pub.dev"
source: hosted
version: "0.3.0"
re_highlight:
dependency: "direct main"
description:
name: re_highlight
sha256: "6c4ac3f76f939fb7ca9df013df98526634e17d8f7460e028bd23a035870024f2"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:

View File

@@ -1,7 +1,7 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.22 version: 0.8.28
environment: environment:
sdk: '>=3.1.0 <4.0.0' sdk: '>=3.1.0 <4.0.0'
@@ -17,7 +17,7 @@ dependencies:
provider: ^6.0.5 provider: ^6.0.5
window_manager: ^0.3.8 window_manager: ^0.3.8
ffi: ^2.1.0 ffi: ^2.1.0
dynamic_color: ^1.6.0 dynamic_color: ^1.7.0
proxy: proxy:
path: plugins/proxy path: plugins/proxy
launch_at_startup: ^0.2.2 launch_at_startup: ^0.2.2
@@ -39,6 +39,8 @@ dependencies:
webdav_client: ^1.2.2 webdav_client: ^1.2.2
dio: ^5.4.3+1 dio: ^5.4.3+1
country_flags: ^2.2.0 country_flags: ^2.2.0
re_editor: ^0.3.0
re_highlight: ^0.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View File

@@ -61,21 +61,21 @@ class Build {
arch: Arch.amd64, arch: Arch.amd64,
archName: 'amd64', archName: 'amd64',
), ),
// BuildLibItem( BuildLibItem(
// platform: PlatformType.android, platform: PlatformType.android,
// arch: Arch.arm, arch: Arch.arm,
// archName: 'armeabi-v7a', archName: 'armeabi-v7a',
// ), ),
BuildLibItem( BuildLibItem(
platform: PlatformType.android, platform: PlatformType.android,
arch: Arch.arm64, arch: Arch.arm64,
archName: 'arm64-v8a', archName: 'arm64-v8a',
), ),
// BuildLibItem( BuildLibItem(
// platform: PlatformType.android, platform: PlatformType.android,
// arch: Arch.amd64, arch: Arch.amd64,
// archName: 'x86_64', archName: 'x86_64',
// ), ),
BuildLibItem( BuildLibItem(
platform: PlatformType.linux, platform: PlatformType.linux,
arch: Arch.amd64, arch: Arch.amd64,