Compare commits

...

4 Commits

Author SHA1 Message Date
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
65 changed files with 3180 additions and 953 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -13,6 +13,7 @@ const (
Now MessageType = "now"
Process MessageType = "process"
Request MessageType = "request"
Run MessageType = "run"
)
type Message struct {
@@ -20,11 +21,18 @@ type Message struct {
Data interface{} `json:"data"`
}
func (message *Message) Json() string {
data, _ := json.Marshal(message)
return string(data)
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}
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))
}
//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
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)

View File

@@ -1,42 +0,0 @@
//go:build android
package platform
import "syscall"
var nullFd int
var maxFdCount int
func init() {
fd, err := syscall.Open("/dev/null", syscall.O_WRONLY, 0644)
if err != nil {
panic(err.Error())
}
nullFd = fd
var limit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil {
maxFdCount = 1024
} else {
maxFdCount = int(limit.Cur)
}
maxFdCount = maxFdCount / 4 * 3
}
func ShouldBlockConnection() bool {
fd, err := syscall.Dup(nullFd)
if err != nil {
return true
}
_ = syscall.Close(fd)
if fd > maxFdCount {
return true
}
return false
}

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

View File

@@ -4,13 +4,10 @@ package main
import "C"
import (
"core/platform"
t "core/tun"
"errors"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"strconv"
"sync"
"syscall"
"time"
@@ -18,16 +15,11 @@ import (
var tunLock sync.Mutex
var tun *t.Tun
var runTime *time.Time
//export startTUN
func startTUN(fd C.int) {
tunLock.Lock()
now := time.Now()
runTime = &now
go func() {
tunLock.Lock()
defer tunLock.Unlock()
if tun != nil {
@@ -51,26 +43,13 @@ func startTUN(fd C.int) {
tempTun.Closer = closer
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
func stopTun() {
tunLock.Lock()
runTime = nil
go func() {
tunLock.Lock()
defer tunLock.Unlock()
if tun != nil {
@@ -82,9 +61,6 @@ func stopTun() {
func init() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errors.New("blocked")
}
return conn.Control(func(fd uintptr) {
if tun != nil {
tun.MarkSocket(int(fd))

View File

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

View File

@@ -983,6 +983,24 @@ class ClashFFI {
late final _getTraffic =
_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(
ffi.Pointer<ffi.Char> s,
int port,
@@ -1156,16 +1174,6 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>('startTUN');
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() {
return _stopTun();
}

View File

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

View File

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

View File

@@ -8,12 +8,30 @@ import 'constant.dart';
class AppPath {
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() {
getApplicationSupportDirectory().then(
(value) => applicationSupportDirectoryCompleter.complete(value),
);
getApplicationSupportDirectory().then((value) {
cacheDir.complete(value);
});
// if (Platform.isAndroid) {
// getApplicationSupportDirectory().then((value) {
// cacheDir.complete(value);
// });
// } else {
// _createDesktopCacheDir().then((value) {
// cacheDir.complete(value);
// });
// }
}
factory AppPath() {
@@ -22,12 +40,12 @@ class AppPath {
}
Future<String> getHomeDirPath() async {
final directory = await applicationSupportDirectoryCompleter.future;
final directory = await cacheDir.future;
return directory.path;
}
Future<String> getProfilesPath() async {
final directory = await applicationSupportDirectoryCompleter.future;
final directory = await cacheDir.future;
return join(directory.path, profilesDirectoryName);
}

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -31,6 +32,7 @@ class AppController {
Future<void> updateSystemProxy(bool isStart) async {
if (isStart) {
await globalState.startSystemProxy(
appState: appState,
config: config,
clashConfig: clashConfig,
);
@@ -42,7 +44,9 @@ class AppController {
];
} else {
await globalState.stopSystemProxy();
clashCore.resetTraffic();
appState.traffics = [];
appState.totalTraffic = Traffic();
appState.runTime = null;
}
}
@@ -97,13 +101,9 @@ class AppController {
}
}
Future<void> updateProfile(String id) async {
final profile = config.getCurrentProfileForId(id);
if (profile != null) {
final tempProfile = profile.copyWith();
await tempProfile.update();
config.setProfile(tempProfile);
}
Future<void> updateProfile(Profile profile) async {
await profile.update();
config.setProfile(await profile.update());
}
Future<void> updateClashConfig({bool isPatch = true}) async {
@@ -146,7 +146,7 @@ class AppController {
continue;
}
try {
await updateProfile(profile.id);
updateProfile(profile);
} catch (e) {
appState.addLog(
Log(
@@ -163,7 +163,7 @@ class AppController {
if (profile.type == ProfileType.file) {
continue;
}
await updateProfile(profile.id);
await updateProfile(profile);
}
}
@@ -278,7 +278,7 @@ class AppController {
config: config,
clashConfig: clashConfig,
);
},title: appLocalizations.init);
}, title: appLocalizations.init);
} else {
await globalState.applyProfile(
appState: appState,
@@ -366,11 +366,9 @@ class AppController {
if (commonScaffoldState?.mounted != true) return;
final profile = await commonScaffoldState?.loadingRun<Profile>(
() async {
final profile = Profile(
return await Profile.normal(
url: url,
);
await profile.update();
return profile;
).update();
},
title: "${appLocalizations.add}${appLocalizations.profile}",
);
@@ -393,9 +391,7 @@ class AppController {
if (bytes == null) {
return null;
}
final profile = Profile(label: platformFile?.name);
await profile.saveFile(bytes);
return profile;
return await Profile.normal(label: platformFile?.name).saveFile(bytes);
},
title: "${appLocalizations.add}${appLocalizations.profile}",
);

View File

@@ -56,7 +56,7 @@ enum ProfileType { file, url }
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 }
@@ -64,3 +64,8 @@ enum RecoveryOption {
all,
onlyProfiles,
}
enum ChipType {
action,
delete,
}

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
}
}
_buildAppSection() {
Widget _buildAppSection() {
final items = [
if (Platform.isAndroid)
Selector<Config, bool>(
@@ -150,7 +150,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
}
_buildGeneralSection() {
Widget _buildGeneralSection() {
final items = [
Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
@@ -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
Widget build(BuildContext context) {
List<Widget> items = [
_buildAppSection(),
_buildGeneralSection(),
_buildMoreSection(),
];
return ListView.builder(
padding: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.only(bottom: 32),
itemBuilder: (_, index) {
return Container(
alignment: Alignment.center,

View File

@@ -1,140 +1,435 @@
// import 'dart:async';
//
// import 'package:fl_clash/clash/core.dart';
// import 'package:fl_clash/models/models.dart';
// import 'package:fl_clash/plugins/app.dart';
// import 'package:fl_clash/state.dart';
// import 'package:fl_clash/widgets/widgets.dart';
// import 'package:flutter/material.dart';
//
// class ConnectionsFragment extends StatefulWidget {
// const ConnectionsFragment({super.key});
//
// @override
// State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
// }
//
// class _ConnectionsFragmentState extends State<ConnectionsFragment> {
// final connectionsNotifier = ValueNotifier<List<Connection>>([]);
// Map<String, String?> idPackageNameMap = {};
//
// Timer? timer;
//
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// _getConnections();
// if (timer != null) {
// timer?.cancel();
// timer = null;
// }
// timer = Timer.periodic(const Duration(seconds: 3), (timer) {
// if (mounted) {
// _getConnections();
// }
// });
// });
// }
//
// _getConnections() {
// connectionsNotifier.value = clashCore
// .getConnections();
// }
//
// @override
// void dispose() {
// super.dispose();
// timer?.cancel();
// timer = null;
// }
//
// Future<ImageProvider?> _getPackageIconWithConnection(
// Connection connection) async {
// final uid = connection.metadata.uid;
// // if(globalState.packageNameMap[uid] == null){
// // globalState.packageNameMap[uid] = await app?.getPackageName(connection.metadata);
// // }
// final packageName = globalState.packageNameMap[uid];
// if(packageName == null) return null;
// return await app?.getPackageIcon(packageName);
// }
//
// @override
// Widget build(BuildContext context) {
// return ValueListenableBuilder<List<Connection>>(
// valueListenable: connectionsNotifier,
// builder: (_, List<Connection> connections, __) {
// if (connections.isEmpty) {
// return const NullStatus(
// label: "未开启代理,或者没有连接数据",
// );
// }
// return ListView.separated(
// physics: const AlwaysScrollableScrollPhysics(),
// itemBuilder: (_, index) {
// final connection = connections[index];
// return ListTile(
// titleAlignment: ListTileTitleAlignment.top,
// leading: Container(
// margin: const EdgeInsets.only(top: 4),
// width: 48,
// height: 48,
// child: FutureBuilder<ImageProvider?>(
// future: _getPackageIconWithConnection(connection),
// builder: (_, snapshot) {
// if (!snapshot.hasData && snapshot.data == null) {
// return Container();
// } else {
// return Image(
// image: snapshot.data!,
// gaplessPlayback: true,
// width: 48,
// height: 48,
// );
// }
// },
// ),
// ),
// contentPadding:
// const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
// title: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(connection.metadata.host.isNotEmpty
// ? connection.metadata.host
// : connection.metadata.destinationIP),
// Padding(
// padding: const EdgeInsets.only(
// top: 12,
// ),
// child: Wrap(
// runSpacing: 8,
// spacing: 8,
// children: [
// for (final chain in connection.chains)
// CommonChip(
// label: chain,
// ),
// ],
// ),
// ),
// ],
// ),
// trailing: IconButton(
// icon: const Icon(Icons.block),
// onPressed: () {},
// ),
// );
// },
// separatorBuilder: (BuildContext context, int index) {
// return const Divider(
// height: 0,
// );
// },
// itemCount: connections.length,
// );
// },
// );
// }
// }
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ConnectionsFragment extends StatefulWidget {
const ConnectionsFragment({super.key});
@override
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
}
class _ConnectionsFragmentState extends State<ConnectionsFragment> {
final connectionsNotifier =
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
final ScrollController _scrollController = ScrollController(
keepScrollOffset: false,
);
Timer? timer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(
const Duration(seconds: 1),
(timer) {
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
},
);
});
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
showSearch(
context: context,
delegate: ConnectionsSearchDelegate(
state: connectionsNotifier.value,
),
);
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
)
];
},
);
}
_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
void dispose() {
super.dispose();
timer?.cancel();
connectionsNotifier.dispose();
_scrollController.dispose();
timer = null;
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'connections' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: ValueListenableBuilder<ConnectionsAndKeywords>(
valueListenable: connectionsNotifier,
builder: (_, state, __) {
var connections = state.filteredConnections;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullConnectionsDesc,
);
}
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 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

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

View File

@@ -0,0 +1,86 @@
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.type == InternetAddressType.IPv4 && !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,
),
child: Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
height: globalState.appController.measure.titleLargeHeight + 16,
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(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
)
: const Padding(
padding: EdgeInsets.all(4),
child: CircularProgressIndicator(),
),
);
},
),
),
);
}
}

View File

@@ -19,6 +19,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
final timeoutNotifier = ValueNotifier<bool>(false);
bool? _preIsStart;
CancelToken? cancelToken;
Function? _checkIpDebounce;
_checkIp(
bool isInit,
@@ -44,6 +45,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
}
_checkIpContainer(Widget child) {
_checkIpDebounce = debounce(_checkIp);
return Selector2<AppState, Config, CheckIpSelectorState>(
selector: (_, appState, config) {
return CheckIpSelectorState(
@@ -53,13 +55,22 @@ class _NetworkDetectionState extends State<NetworkDetection> {
);
},
builder: (_, state, __) {
_checkIp(state.isInit, state.isStart);
if (_checkIpDebounce != null) {
_checkIpDebounce!([state.isInit, state.isStart]);
}
return child;
},
child: child,
);
}
@override
void dispose() {
super.dispose();
ipInfoNotifier.dispose();
timeoutNotifier.dispose();
}
@override
Widget build(BuildContext context) {
return _checkIpContainer(

View File

@@ -21,26 +21,17 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
.asMap()
.map(
(index, e) => MapEntry(
index,
Point(
(index + initPoints.length).toDouble(),
e.speed.toDouble(),
),
),
)
index,
Point(
(index + initPoints.length).toDouble(),
e.speed.toDouble(),
),
),
)
.values
.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) {
@@ -53,11 +44,10 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
required IconData iconData,
required TrafficValue value,
}) {
final showValue = value.showValue;
final showUnit = "${value.showUnit}/s";
final titleLargeSoftBold =
Theme.of(context).textTheme.titleLarge?.toSoftBold();
Theme.of(context).textTheme.titleLarge?.toSoftBold();
final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight();
final valueText = Text(
showValue,
@@ -121,7 +111,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
@override
Widget build(BuildContext context) {
return CommonCard(
info: Info(
info: Info(
label: appLocalizations.networkSpeed,
iconData: Icons.speed,
),
@@ -172,4 +162,4 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
),
);
}
}
}

View File

@@ -55,21 +55,11 @@ class TrafficUsage extends StatelessWidget {
label: appLocalizations.trafficUsage,
iconData: Icons.data_saver_off,
),
child: Selector<AppState, List<Traffic>>(
selector: (_, appState) => appState.traffics,
builder: (_, traffics, __) {
final trafficTotal = traffics.isNotEmpty
? traffics.reduce(
(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;
child: Selector<AppState, Traffic>(
selector: (_, appState) => appState.totalTraffic,
builder: (_, totalTraffic, __) {
final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down;
return Padding(
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: Column(
@@ -80,7 +70,7 @@ class TrafficUsage extends StatelessWidget {
child: getTrafficDataItem(
context,
Icons.arrow_upward,
upTrafficValue,
upTotalTrafficValue,
),
),
const SizedBox(
@@ -91,7 +81,7 @@ class TrafficUsage extends StatelessWidget {
child: getTrafficDataItem(
context,
Icons.arrow_downward,
downTrafficValue,
downTotalTrafficValue,
),
),
],

View File

@@ -17,7 +17,12 @@ class LogsFragment extends StatefulWidget {
}
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;
@override
@@ -25,18 +30,18 @@ class _LogsFragmentState extends State<LogsFragment> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
logsNotifier.value = List<Log>.from(appState.logs);
logsNotifier.value = logsNotifier.value.copyWith(logs: appState.logs);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final logs = List<Log>.from(appState.logs);
final logs = appState.logs;
if (!const ListEquality<Log>().equals(
logsNotifier.value,
logsNotifier.value.logs,
logs,
)) {
logsNotifier.value = logs;
logsNotifier.value = logsNotifier.value.copyWith(logs: logs);
}
});
});
@@ -46,6 +51,8 @@ class _LogsFragmentState extends State<LogsFragment> {
void dispose() {
super.dispose();
timer?.cancel();
logsNotifier.dispose();
scrollController.dispose();
timer = null;
}
@@ -59,7 +66,7 @@ class _LogsFragmentState extends State<LogsFragment> {
showSearch(
context: context,
delegate: LogsSearchDelegate(
logs: logsNotifier.value.reversed.toList(),
logs: logsNotifier.value,
),
);
},
@@ -72,32 +79,23 @@ class _LogsFragmentState extends State<LogsFragment> {
});
}
_buildList() {
return ValueListenableBuilder<List<Log>>(
valueListenable: logsNotifier,
builder: (_, List<Log> logs, __) {
if (logs.isEmpty) {
return NullStatus(
label: appLocalizations.nullLogsDesc,
);
}
logs = logs.reversed.toList();
return ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: logs.length,
itemBuilder: (BuildContext context, int index) {
final log = logs[index];
return LogItem(
log: log,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
);
},
_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,
);
}
@@ -114,21 +112,88 @@ class _LogsFragmentState extends State<LogsFragment> {
}
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 {
List<Log> logs = [];
ValueNotifier<LogsAndKeywords> logsNotifier;
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 {
final lowQuery = query.toLowerCase();
return logs
return logsNotifier.value.filteredLogs
.where(
(log) =>
(log.payload?.toLowerCase().contains(lowQuery) ?? false) ||
@@ -171,37 +236,98 @@ class LogsSearchDelegate extends SearchDelegate {
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
Widget buildSuggestions(BuildContext context) {
return ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: _results.length,
itemBuilder: (BuildContext context, int index) {
final log = _results[index];
return LogItem(
key: ValueKey(log.dateTime),
log: log,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
return ValueListenableBuilder(
valueListenable: logsNotifier,
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 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 Function(String)? onClick;
const LogItem({
super.key,
required this.log,
this.onClick,
});
@override
State<LogItem> createState() => _LogItemState();
}
class _LogItemState extends State<LogItem> {
@override
Widget build(BuildContext context) {
final log = widget.log;
return ListTile(
title: SelectableText(log.payload ?? ''),
subtitle: Column(
@@ -223,6 +349,10 @@ class LogItem extends StatelessWidget {
vertical: 8,
),
child: CommonChip(
onPressed: () {
if (widget.onClick == null) return;
widget.onClick!(log.logLevel.name);
},
label: log.logLevel.name,
),
),

View File

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

View File

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

View File

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

View File

@@ -64,13 +64,18 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
final indexIsChanging = _tabController?.indexIsChanging ?? false;
if (indexIsChanging) return;
final index = _tabController?.index;
if(index == null) return;
if (index == null) return;
final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups;
if (currentGroups.length > index) {
appController.config.updateCurrentGroupName(currentGroups[index].name);
}
}
@override
void dispose() {
super.dispose();
_tabController?.dispose();
}
@override
Widget build(BuildContext context) {
@@ -120,7 +125,8 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor: const WidgetStatePropertyAll(Colors.transparent),
overlayColor:
const WidgetStatePropertyAll(Colors.transparent),
tabs: [
for (final groupName in state.groupNames)
Tab(

View File

@@ -3,21 +3,24 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class RequestFragment extends StatefulWidget {
const RequestFragment({super.key});
class RequestsFragment extends StatefulWidget {
const RequestsFragment({super.key});
@override
State<RequestFragment> createState() => _RequestFragmentState();
State<RequestsFragment> createState() => _RequestsFragmentState();
}
class _RequestFragmentState extends State<RequestFragment> {
final requestsNotifier = ValueNotifier<List<Connection>>([]);
class _RequestsFragmentState extends State<RequestsFragment> {
final requestsNotifier =
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
final ScrollController _scrollController = ScrollController(
keepScrollOffset: false,
);
@@ -29,23 +32,70 @@ class _RequestFragmentState extends State<RequestFragment> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
requestsNotifier.value = List<Connection>.from(appState.requests);
requestsNotifier.value =
requestsNotifier.value.copyWith(connections: appState.requests);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final requests = List<Connection>.from(appState.requests);
final requests = appState.requests;
if (!const ListEquality<Connection>().equals(
requestsNotifier.value,
requestsNotifier.value.connections,
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
void dispose() {
super.dispose();
@@ -56,42 +106,86 @@ class _RequestFragmentState extends State<RequestFragment> {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<List<Connection>>(
valueListenable: requestsNotifier,
builder: (_, List<Connection> connections, __) {
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'requests' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
connections = connections.reversed.toList();
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,
);
return child!;
},
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 {
final Connection connection;
final Function(String)? onClick;
const RequestItem({
super.key,
required this.connection,
this.onClick,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
@@ -119,8 +213,8 @@ class RequestItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
titleAlignment: ListTileTitleAlignment.top,
return ListItem(
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: Platform.isAndroid
? Container(
margin: const EdgeInsets.only(top: 4),
@@ -165,6 +259,10 @@ class RequestItem extends StatelessWidget {
for (final chain in connection.chains)
CommonChip(
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(
padding: EdgeInsets.only(left: 12,right: 4),
child: VerticalDivider(
endIndent: 2,
endIndent: 6,
width: 4,
indent: 2,
indent: 6,
),
),
externalProvider.vehicleType == "HTTP"

View File

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

View File

@@ -179,8 +179,18 @@
"geodataLoaderDesc": "Enabling will use the Geo low memory loader",
"requests": "Requests",
"requestsDesc": "View recently requested data",
"nullRequestsDesc": "No proxy or no request",
"findProcessMode": "Find process",
"findProcessModeDesc": "There is a risk of flashback after opening",
"init": "Init"
"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

@@ -179,8 +179,18 @@
"geodataLoaderDesc": "开启将使用Geo低内存加载器",
"requests": "请求",
"requestsDesc": "查看最近请求数据",
"nullRequestsDesc": "未开启代理或者没有请求",
"findProcessMode": "查找进程",
"findProcessModeDesc": "开启后存在闪退风险",
"init": "初始化"
"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(
"Opening it will lose part of its application ability and gain the support of full amount of Clash."),
"confirm": MessageLookupByLibrary.simpleMessage("Confirm"),
"connections": MessageLookupByLibrary.simpleMessage("Connections"),
"connectionsDesc":
MessageLookupByLibrary.simpleMessage("View current connection"),
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"),
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
"core": MessageLookupByLibrary.simpleMessage("Core"),
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
"country": MessageLookupByLibrary.simpleMessage("Country"),
"create": MessageLookupByLibrary.simpleMessage("Create"),
"cut": MessageLookupByLibrary.simpleMessage("Cut"),
"dark": MessageLookupByLibrary.simpleMessage("Dark"),
"dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"),
"days": MessageLookupByLibrary.simpleMessage("Days"),
@@ -117,6 +122,8 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"),
"exit": MessageLookupByLibrary.simpleMessage("Exit"),
"expirationTime":
MessageLookupByLibrary.simpleMessage("Expiration time"),
"externalController":
MessageLookupByLibrary.simpleMessage("ExternalController"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
@@ -142,7 +149,10 @@ class MessageLookup extends MessageLookupByLibrary {
"hours": MessageLookupByLibrary.simpleMessage("Hours"),
"importFromURL":
MessageLookupByLibrary.simpleMessage("Import from URL"),
"infiniteTime":
MessageLookupByLibrary.simpleMessage("Long term effective"),
"init": MessageLookupByLibrary.simpleMessage("Init"),
"intranetIp": MessageLookupByLibrary.simpleMessage("Intranet IP"),
"ipCheckTimeout":
MessageLookupByLibrary.simpleMessage("Ip check timeout"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
@@ -175,13 +185,14 @@ class MessageLookup extends MessageLookupByLibrary {
"Please create a profile or add a valid profile"),
"notSelectedTip": MessageLookupByLibrary.simpleMessage(
"The current proxy group cannot be selected."),
"nullConnectionsDesc":
MessageLookupByLibrary.simpleMessage("No Connections"),
"nullCoreInfoDesc":
MessageLookupByLibrary.simpleMessage("Unable to obtain core info"),
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("No logs"),
"nullProfileDesc": MessageLookupByLibrary.simpleMessage(
"No profile, Please add a profile"),
"nullRequestsDesc":
MessageLookupByLibrary.simpleMessage("No proxy or no request"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
"other": MessageLookupByLibrary.simpleMessage("Other"),
"outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"),
"override": MessageLookupByLibrary.simpleMessage("Override"),
@@ -190,6 +201,7 @@ class MessageLookup extends MessageLookupByLibrary {
"password": MessageLookupByLibrary.simpleMessage("Password"),
"passwordTip":
MessageLookupByLibrary.simpleMessage("Password cannot be empty"),
"paste": MessageLookupByLibrary.simpleMessage("Paste"),
"pleaseBindWebDAV":
MessageLookupByLibrary.simpleMessage("Please bind WebDAV"),
"pleaseUploadFile":
@@ -282,6 +294,7 @@ class MessageLookup extends MessageLookupByLibrary {
"url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc":
MessageLookupByLibrary.simpleMessage("Obtain profile through URL"),
"view": MessageLookupByLibrary.simpleMessage("View"),
"webDAVConfiguration":
MessageLookupByLibrary.simpleMessage("WebDAV configuration"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"),

View File

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

View File

@@ -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`
String get findProcessMode {
return Intl.message(
@@ -1889,6 +1879,116 @@ class AppLocalizations {
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> {

View File

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

View File

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

View File

@@ -143,18 +143,21 @@ class Config extends ChangeNotifier {
}
Profile? get currentProfile {
try {
return profiles.firstWhere((element) => element.id == _currentProfileId);
} catch (_) {
return null;
}
final index =
profiles.indexWhere((profile) => profile.id == _currentProfileId);
return index == -1 ? null : profiles[index];
}
String? get currentGroupName => currentProfile?.currentGroupName;
updateCurrentGroupName(String groupName) {
if (currentProfile?.currentGroupName != groupName) {
currentProfile?.currentGroupName = groupName;
if (currentProfile != null &&
currentProfile!.currentGroupName != groupName) {
_setProfile(
currentProfile!.copyWith(
currentGroupName: groupName,
),
);
notifyListeners();
}
}
@@ -164,9 +167,16 @@ class Config extends ChangeNotifier {
}
updateCurrentSelectedMap(String groupName, String proxyName) {
if (currentProfile?.selectedMap[groupName] != proxyName) {
currentProfile?.selectedMap = Map.from(currentProfile?.selectedMap ?? {})
..[groupName] = proxyName;
if (currentProfile != null &&
currentProfile!.selectedMap[groupName] != proxyName) {
final SelectedMap selectedMap = Map.from(
currentProfile?.selectedMap ?? {},
)..[groupName] = proxyName;
_setProfile(
currentProfile!.copyWith(
selectedMap: selectedMap,
),
);
notifyListeners();
}
}

View File

@@ -23,7 +23,7 @@ class Metadata with _$Metadata {
}
@freezed
class Connection with _$Connection{
class Connection with _$Connection {
const factory Connection({
required String id,
num? upload,
@@ -36,3 +36,19 @@ class Connection with _$Connection{
factory Connection.fromJson(Map<String, Object?> 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 {
const factory ProcessMapItem({
required int id,
String? value,
required String value,
}) = _ProcessMapItem;
factory ProcessMapItem.fromJson(Map<String, Object?> json) =>

View File

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

View File

@@ -57,6 +57,7 @@ const _$MessageTypeEnumMap = {
MessageType.process: 'process',
MessageType.now: 'now',
MessageType.request: 'request',
MessageType.run: 'run',
};
_$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl(
@@ -95,7 +96,7 @@ Map<String, dynamic> _$$ProcessImplToJson(_$ProcessImpl instance) =>
_$ProcessMapItemImpl _$$ProcessMapItemImplFromJson(Map<String, dynamic> json) =>
_$ProcessMapItemImpl(
id: (json['id'] as num).toInt(),
value: json['value'] as String?,
value: json['value'] as String,
);
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.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,546 @@
// 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;
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});
$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,
}) {
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>,
) 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});
@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,
}) {
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>,
));
}
}
/// @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 {}})
: _selectedMap = selectedMap;
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);
}
@override
String toString() {
return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap)';
}
@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));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
id,
label,
currentGroupName,
url,
lastUpdateDate,
autoUpdateDuration,
userInfo,
autoUpdate,
const DeepCollectionEquality().hash(_selectedMap));
@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}) = _$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
@JsonKey(ignore: true)
_$$ProfileImplCopyWith<_$ProfileImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

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

View File

@@ -1,8 +1,12 @@
// ignore_for_file: invalid_annotation_target
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.freezed.dart';
@JsonSerializable()
class Log {
@JsonKey(name: "LogLevel")
@@ -31,3 +35,22 @@ class Log {
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/enum/enum.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.freezed.dart';
typedef SelectedMap = Map<String, String>;
@JsonSerializable()
class UserInfo {
int upload;
int download;
int total;
int expire;
@freezed
class UserInfo with _$UserInfo {
const factory UserInfo({
@Default(0) int upload,
@Default(0) int download,
@Default(0) int total,
@Default(0) int expire,
}) = _UserInfo;
UserInfo({
int? upload,
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.fromJson(Map<String, Object?> json) =>
_$UserInfoFromJson(json);
factory UserInfo.formHString(String? info) {
if (info == null) return UserInfo();
if (info == null) return const UserInfo();
final list = info.split(";");
Map<String, int?> map = {};
for (final i in list) {
@@ -45,73 +34,76 @@ class UserInfo {
map[keyValue[0]] = int.tryParse(keyValue[1]);
}
return UserInfo(
upload: map["upload"],
download: map["download"],
total: map["total"],
expire: map["expire"],
upload: map["upload"] ?? 0,
download: map["download"] ?? 0,
total: map["total"] ?? 0,
expire: map["expire"] ?? 0,
);
}
@override
String toString() {
return 'UserInfo{upload: $upload, download: $download, total: $total, expire: $expire}';
}
}
@JsonSerializable()
class Profile {
String id;
String? label;
String? currentGroupName;
String? url;
DateTime? lastUpdateDate;
Duration autoUpdateDuration;
UserInfo? userInfo;
bool autoUpdate;
SelectedMap selectedMap;
@freezed
class Profile with _$Profile {
const factory Profile({
required String id,
String? label,
String? currentGroupName,
@Default("") String url,
DateTime? lastUpdateDate,
required Duration autoUpdateDuration,
UserInfo? userInfo,
@Default(true) bool autoUpdate,
@Default({}) SelectedMap selectedMap,
}) = _Profile;
Profile({
String? id,
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.fromJson(Map<String, Object?> json) =>
_$ProfileFromJson(json);
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 =>
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 {
final isExists = await check();
if (!isExists) {
if (url != null) {
return await update();
if (url.isNotEmpty) {
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 {
final profilePath = await appPath.getProfilePath(id);
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));
if (message.isNotEmpty) {
throw message;
@@ -123,66 +115,21 @@ class Profile {
await file.create(recursive: true);
}
await file.writeAsBytes(bytes);
lastUpdateDate = DateTime.now();
return copyWith(lastUpdateDate: DateTime.now());
}
Map<String, dynamic> toJson() {
return _$ProfileToJson(this);
}
factory Profile.fromJson(Map<String, dynamic> json) {
return _$ProfileFromJson(json);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Profile &&
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,
);
Future<Profile> saveFileWithString(String value) async {
final message = await clashCore.validateConfig(value);
if (message.isNotEmpty) {
throw message;
}
final path = await appPath.getProfilePath(id);
final file = File(path!);
final isExists = await file.exists();
if (!isExists) {
await file.create(recursive: true);
}
await file.writeAsString(value);
return copyWith(lastUpdateDate: DateTime.now());
}
}

View File

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

@@ -57,6 +57,7 @@ class GlobalState {
}
Future<void> startSystemProxy({
required AppState appState,
required Config config,
required ClashConfig clashConfig,
}) async {
@@ -73,6 +74,11 @@ class GlobalState {
args: args,
);
startListenUpdate();
applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
Future<void> stopSystemProxy() async {
@@ -195,6 +201,7 @@ class GlobalState {
final traffic = clashCore.getTraffic();
if (appState != null) {
appState.addTraffic(traffic);
appState.totalTraffic = clashCore.getTotalTraffic();
}
if (Platform.isAndroid) {
final currentProfile = config.currentProfile;

View File

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

View File

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

View File

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

View File

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

View File

@@ -517,6 +517,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -705,10 +721,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a
url: "https://pub.dev"
source: hosted
version: "2.2.5"
version: "2.2.6"
path_provider_foundation:
dependency: transitive
description:
@@ -820,6 +836,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View File

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