Optimize ip detection

Support android vpn ipv6 inbound switch

Support log export

Optimize more details
This commit is contained in:
chen08209
2024-09-20 14:32:57 +08:00
parent 13d31cf708
commit 4e3dc45f13
55 changed files with 34939 additions and 30259 deletions

View File

@@ -1,10 +1,13 @@
import 'dart:io';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
class Android {
init() async {
app?.onExit = () {};
app?.onExit = () async {
await globalState.appController.savePreferences();
};
}
}

View File

@@ -28,4 +28,5 @@ export 'iterable.dart';
export 'scroll.dart';
export 'icons.dart';
export 'http.dart';
export 'keyboard.dart';
export 'keyboard.dart';
export 'network.dart';

View File

@@ -26,9 +26,9 @@ const GeoXMap defaultGeoXMap = {
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":
"https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb",
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat",
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
"geosite":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
};

View File

@@ -15,9 +15,6 @@ class FlClashHttpOverrides extends HttpOverrides {
final port = appController.clashConfig.mixedPort;
final isStart = appController.appFlowingState.isStart;
if (!isStart) return "DIRECT";
if (appController.appState.groups.isEmpty) {
return "DIRECT";
}
return "PROXY localhost:$port";
};
return client;

View File

@@ -15,4 +15,10 @@ extension ListExtension<T> on List<T> {
}
return res;
}
List<T> safeSublist(int start) {
if(start <= 0) return this;
if(start > length) return [];
return sublist(start);
}
}

25
lib/common/network.dart Normal file
View File

@@ -0,0 +1,25 @@
import 'dart:io';
extension NetworkInterfaceExt on NetworkInterface {
bool get isWifi {
final nameLowCase = name.toLowerCase();
if (nameLowCase.contains('wlan') ||
nameLowCase.contains('wi-fi') ||
nameLowCase == 'en0' ||
nameLowCase == 'eth0') {
return true;
}
return false;
}
bool get includesIPv4 {
return addresses.any((addr) => addr.isIPv4);
}
}
extension InternetAddressExt on InternetAddress {
bool get isIPv4 {
return type == InternetAddressType.IPv4;
}
}

View File

@@ -171,14 +171,18 @@ class Other {
if (disposition == null) return null;
final parseValue = HeaderValue.parse(disposition);
final parameters = parseValue.parameters;
final key = parameters.keys
.firstWhere((key) => key.startsWith("filename"), orElse: () => '');
if (key.isEmpty) return null;
if (key == "filename*") {
return Uri.decodeComponent((parameters[key] ?? "").split("'").last);
} else {
return parameters[key];
final fileNamePointKey = parameters.keys
.firstWhere((key) => key == "filename*", orElse: () => "");
if (fileNamePointKey.isNotEmpty) {
final res = parameters[fileNamePointKey]?.split("''") ?? [];
if (res.length >= 2) {
return Uri.decodeComponent(res[1]);
}
}
final fileNameKey = parameters.keys
.firstWhere((key) => key == "filename", orElse: () => "");
if (fileNameKey.isEmpty) return null;
return parameters[fileNameKey];
}
double getViewWidth() {
@@ -221,11 +225,14 @@ class Other {
return "${appName}_backup_${DateTime.now().show}.zip";
}
String get logFile {
return "${appName}_${DateTime.now().show}.log";
}
Size getScreenSize() {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
}
final other = Other();

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/services.dart';
import 'window.dart';

View File

@@ -2,10 +2,12 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:fl_clash/common/archive.dart';
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:path/path.dart';
@@ -58,7 +60,9 @@ class AppController {
updateRunTime,
updateTraffic,
];
applyProfileDebounce();
if (!Platform.isAndroid) {
applyProfileDebounce();
}
} else {
await globalState.handleStop();
clashCore.resetTraffic();
@@ -118,11 +122,15 @@ class AppController {
}
Future<void> updateClashConfig({bool isPatch = true}) async {
await globalState.updateClashConfig(
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
);
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await globalState.updateClashConfig(
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
);
});
}
Future applyProfile({bool isPrue = false}) async {
@@ -299,7 +307,7 @@ class AppController {
init() async {
final isDisclaimerAccepted = await handlerDisclaimer();
if (!isDisclaimerAccepted) {
system.exit();
handleExit();
}
updateLogStatus();
if (!config.silentLaunch) {
@@ -520,21 +528,6 @@ class AppController {
'';
}
Future<List<int>> backupData() async {
final homeDirPath = await appPath.getHomeDirPath();
final profilesPath = await appPath.getProfilesPath();
final configJson = config.toJson();
final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async {
final archive = Archive();
archive.add("config.json", configJson);
archive.add("clashConfig.json", clashConfigJson);
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
final zipEncoder = ZipEncoder();
return zipEncoder.encode(archive) ?? [];
});
}
updateTun() {
clashConfig.tun = clashConfig.tun.copyWith(
enable: !clashConfig.tun.enable,
@@ -573,6 +566,36 @@ class AppController {
clashConfig.mode = Mode.values[nextIndex];
}
Future<bool> exportLogs() async {
final logsRaw = appFlowingState.logs.map(
(item) => item.toString(),
);
final data = await Isolate.run<List<int>>(() async {
final logsRawString = logsRaw.join("\n");
return utf8.encode(logsRawString);
});
return await picker.saveFile(
other.logFile,
Uint8List.fromList(data),
) !=
null;
}
Future<List<int>> backupData() async {
final homeDirPath = await appPath.getHomeDirPath();
final profilesPath = await appPath.getProfilesPath();
final configJson = config.toJson();
final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async {
final archive = Archive();
archive.add("config.json", configJson);
archive.add("clashConfig.json", clashConfigJson);
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
final zipEncoder = ZipEncoder();
return zipEncoder.encode(archive) ?? [];
});
}
recoveryData(
List<int> data,
RecoveryOption recoveryOption,

View File

@@ -15,6 +15,10 @@ extension GroupTypeExtension on GroupType {
)
.toList();
bool get isURLTestOrFallback {
return [GroupType.URLTest, GroupType.Fallback].contains(this);
}
static GroupType? getGroupType(String value) {
final index = GroupTypeExtension.valueList.indexOf(value);
if (index == -1) return null;

View File

@@ -67,11 +67,9 @@ class _ConfigFragmentState extends State<ConfigFragment> {
title: const Text("DNS"),
subtitle: Text(appLocalizations.dnsDesc),
leading: const Icon(Icons.dns),
delegate: OpenDelegate(
delegate: const OpenDelegate(
title: "DNS",
widget: generateListView(
dnsItems,
),
widget: DnsListView(),
isScaffold: true,
isBlur: false,
extendPageWidth: 360,

View File

@@ -10,35 +10,8 @@ import 'package:provider/provider.dart';
class OverrideItem extends StatelessWidget {
const OverrideItem({super.key});
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
globalState.showMessage(
title: appLocalizations.resetDns,
message: TextSpan(
text: appLocalizations.dnsResetTip,
),
onTab: () {
globalState.appController.clashConfig.dns = const Dns();
Navigator.of(context).pop();
});
},
tooltip: appLocalizations.resetDns,
icon: const Icon(
Icons.replay,
),
)
];
});
}
@override
Widget build(BuildContext context) {
_initActions(context);
return Selector<Config, bool>(
selector: (_, config) => config.overrideDns,
builder: (_, override, __) {
@@ -58,35 +31,6 @@ class OverrideItem extends StatelessWidget {
}
}
class DnsDisabledContainer extends StatelessWidget {
final Widget child;
const DnsDisabledContainer(
this.child, {
super.key,
});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.overrideDns,
builder: (_, enable, child) {
return AbsorbPointer(
absorbing: !enable,
child: DisabledMask(
status: !enable,
child: Container(
color: context.colorScheme.surface,
child: child!,
),
),
);
},
child: child,
);
}
}
class StatusItem extends StatelessWidget {
const StatusItem({super.key});
@@ -469,7 +413,6 @@ class NameserverPolicyItem extends StatelessWidget {
items: nameserverPolicy.entries,
titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value),
isMap: true,
onRemove: (value) {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
@@ -795,27 +738,25 @@ class DnsOptions extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DnsDisabledContainer(
Column(
children: generateSection(
title: appLocalizations.options,
items: [
const StatusItem(),
const UseHostsItem(),
const UseSystemHostsItem(),
const IPv6Item(),
const RespectRulesItem(),
const PreferH3Item(),
const DnsModeItem(),
const FakeIpRangeItem(),
const FakeIpFilterItem(),
const DefaultNameserverItem(),
const NameserverPolicyItem(),
const NameserverItem(),
const FallbackItem(),
const ProxyServerNameserverItem(),
],
),
return Column(
children: generateSection(
title: appLocalizations.options,
items: [
const StatusItem(),
const UseHostsItem(),
const UseSystemHostsItem(),
const IPv6Item(),
const RespectRulesItem(),
const PreferH3Item(),
const DnsModeItem(),
const FakeIpRangeItem(),
const FakeIpFilterItem(),
const DefaultNameserverItem(),
const NameserverPolicyItem(),
const NameserverItem(),
const FallbackItem(),
const ProxyServerNameserverItem(),
],
),
);
}
@@ -826,18 +767,16 @@ class FallbackFilterOptions extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DnsDisabledContainer(
Column(
children: generateSection(
title: appLocalizations.fallbackFilter,
items: [
const GeoipItem(),
const GeoipCodeItem(),
const GeositeItem(),
const IpcidrItem(),
const DomainItem(),
],
),
return Column(
children: generateSection(
title: appLocalizations.fallbackFilter,
items: [
const GeoipItem(),
const GeoipCodeItem(),
const GeositeItem(),
const IpcidrItem(),
const DomainItem(),
],
),
);
}
@@ -848,3 +787,41 @@ const dnsItems = <Widget>[
DnsOptions(),
FallbackFilterOptions(),
];
class DnsListView extends StatelessWidget {
const DnsListView({super.key});
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
globalState.showMessage(
title: appLocalizations.resetDns,
message: TextSpan(
text: appLocalizations.dnsResetTip,
),
onTab: () {
globalState.appController.clashConfig.dns = const Dns();
Navigator.of(context).pop();
});
},
tooltip: appLocalizations.resetDns,
icon: const Icon(
Icons.replay,
),
)
];
});
}
@override
Widget build(BuildContext context) {
_initActions(context);
return generateListView(
dnsItems,
);
}
}

View File

@@ -226,7 +226,6 @@ class HostsItem extends StatelessWidget {
clashConfig.hosts = Map.from(clashConfig.hosts)
..addEntries([value]);
},
isMap: true,
);
},
),

View File

@@ -115,6 +115,34 @@ class SystemProxySwitch extends StatelessWidget {
}
}
class Ipv6Switch extends StatelessWidget {
const Ipv6Switch({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
leading: const Icon(Icons.water_outlined),
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6InboundDesc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
ipv6: value,
);
},
),
);
},
);
}
}
class VpnOptions extends StatelessWidget {
const VpnOptions({super.key});
@@ -127,6 +155,7 @@ class VpnOptions extends StatelessWidget {
items: [
const SystemProxySwitch(),
const AllowBypassSwitch(),
const Ipv6Switch(),
],
),
),

View File

@@ -13,16 +13,55 @@ class IntranetIP extends StatefulWidget {
}
class _IntranetIPState extends State<IntranetIP> {
final ipNotifier = ValueNotifier<String>("");
final ipNotifier = ValueNotifier<String?>("");
Future<String?> getLocalIpAddress() async {
List<NetworkInterface> interfaces = await NetworkInterface.list();
for (final interface in interfaces) {
for (final address in interface.addresses) {
if (!address.isLoopback) {
return address.address;
Future<String> getNetworkType() async {
try {
List<NetworkInterface> interfaces = await NetworkInterface.list(
includeLoopback: false,
type: InternetAddressType.any,
);
for (var interface in interfaces) {
if (interface.name.toLowerCase().contains('wlan') ||
interface.name.toLowerCase().contains('wi-fi')) {
return 'WiFi';
}
if (interface.name.toLowerCase().contains('rmnet') ||
interface.name.toLowerCase().contains('ccmni') ||
interface.name.toLowerCase().contains('cellular')) {
return 'Mobile Data';
}
}
return 'Unknown';
} catch (e) {
return 'Error';
}
}
Future<String?> getLocalIpAddress() async {
List<NetworkInterface> interfaces = await NetworkInterface.list(
includeLoopback: false,
)
..sort((a, b) {
if (a.isWifi && !b.isWifi) return -1;
if (!a.isWifi && b.isWifi) return 1;
if (a.includesIPv4 && !b.includesIPv4) return -1;
if (!a.includesIPv4 && b.includesIPv4) return 1;
return 0;
});
for (final interface in interfaces) {
final addresses = interface.addresses;
if (addresses.isEmpty) {
continue;
}
addresses.sort((a, b) {
if (a.isIPv4 && !b.isIPv4) return -1;
if (!a.isIPv4 && b.isIPv4) return 1;
return 0;
});
return addresses.first.address;
}
return null;
}
@@ -48,17 +87,15 @@ class _IntranetIPState extends State<IntranetIP> {
label: appLocalizations.intranetIP,
iconData: Icons.devices,
),
onPressed: (){
},
onPressed: () {},
child: Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
height: globalState.measure.titleLargeHeight + 24 - 2,
height: globalState.measure.titleMediumHeight + 24 - 2,
child: ValueListenableBuilder(
valueListenable: ipNotifier,
builder: (_, value, __) {
return FadeBox(
child: value.isNotEmpty
child: value != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@@ -67,8 +104,9 @@ class _IntranetIPState extends State<IntranetIP> {
flex: 1,
child: TooltipText(
text: Text(
value,
style: context.textTheme.titleLarge?.toSoftBold.toMinus,
value.isNotEmpty ? value : appLocalizations.noNetwork,
style: context
.textTheme.titleLarge?.toSoftBold.toMinus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
@@ -22,6 +24,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
);
bool? _preIsStart;
Function? _checkIpDebounce;
Timer? _setTimeoutTimer;
CancelToken? cancelToken;
_checkIp() async {
@@ -31,6 +34,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
if (!isInit) return;
final isStart = appFlowingState.isStart;
if (_preIsStart == false && _preIsStart == isStart) return;
_clearSetTimeoutTimer();
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
@@ -43,11 +47,32 @@ class _NetworkDetectionState extends State<NetworkDetection> {
cancelToken = CancelToken();
try {
final ipInfo = await request.checkIp(cancelToken: cancelToken);
if (ipInfo != null) {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: ipInfo,
);
return;
}
_setTimeoutTimer = Timer(const Duration(milliseconds: 2000), () {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: null,
);
});
} catch (_) {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: ipInfo,
isTesting: true,
ipInfo: null,
);
} catch (_) {}
}
}
_clearSetTimeoutTimer() {
if(_setTimeoutTimer != null){
_setTimeoutTimer?.cancel();
_setTimeoutTimer = null;
}
}
_checkIpContainer(Widget child) {

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
@@ -30,18 +31,24 @@ class _LogsFragmentState extends State<LogsFragment> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appFlowingState = globalState.appController.appFlowingState;
logsNotifier.value = logsNotifier.value.copyWith(logs: appFlowingState.logs);
logsNotifier.value =
logsNotifier.value.copyWith(logs: appFlowingState.logs);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final logs = appFlowingState.logs;
final maxLength = Platform.isAndroid ? 1000 : 60;
final logs = appFlowingState.logs.safeSublist(
appFlowingState.logs.length - maxLength,
);
if (!const ListEquality<Log>().equals(
logsNotifier.value.logs,
logs,
)) {
logsNotifier.value = logsNotifier.value.copyWith(logs: logs);
logsNotifier.value = logsNotifier.value.copyWith(
logs: logs,
);
}
});
});
@@ -56,6 +63,21 @@ class _LogsFragmentState extends State<LogsFragment> {
timer = null;
}
_handleExport() async {
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
return await globalState.appController.exportLogs();
},
title: appLocalizations.exportLogs,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.exportSuccess),
);
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
@@ -72,6 +94,17 @@ class _LogsFragmentState extends State<LogsFragment> {
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () {
_handleExport();
},
icon: const Icon(
Icons.file_download_outlined,
),
),
];
});
}
@@ -236,7 +269,8 @@ class LogsSearchDelegate extends SearchDelegate {
_addKeyword(String keyword) {
final isContains = logsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(logsNotifier.value.keywords)..add(keyword);
final keywords = List<String>.from(logsNotifier.value.keywords)
..add(keyword);
logsNotifier.value = logsNotifier.value.copyWith(
keywords: keywords,
);
@@ -245,7 +279,8 @@ class LogsSearchDelegate extends SearchDelegate {
_deleteKeyword(String keyword) {
final isContains = logsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(logsNotifier.value.keywords)..remove(keyword);
final keywords = List<String>.from(logsNotifier.value.keywords)
..remove(keyword);
logsNotifier.value = logsNotifier.value.copyWith(
keywords: keywords,
);
@@ -339,7 +374,9 @@ class _LogItemState extends State<LogItem> {
style: context.textTheme.bodySmall
?.copyWith(color: context.colorScheme.primary),
),
const SizedBox(height: 8,),
const SizedBox(
height: 8,
),
Container(
alignment: Alignment.centerLeft,
child: CommonChip(

View File

@@ -102,12 +102,12 @@ class ProxyCard extends StatelessWidget {
_changeProxy(BuildContext context) async {
final appController = globalState.appController;
final isUrlTest = groupType == GroupType.URLTest;
final isURLTestOrFallback = groupType.isURLTestOrFallback;
final isSelector = groupType == GroupType.Selector;
if (isUrlTest || isSelector) {
if (isURLTestOrFallback || isSelector) {
final currentProxyName =
appController.config.currentSelectedMap[groupName];
final nextProxyName = switch (isUrlTest) {
final nextProxyName = switch (isURLTestOrFallback) {
true => currentProxyName == proxy.name ? "" : proxy.name,
false => proxy.name,
};
@@ -207,7 +207,7 @@ class ProxyCard extends StatelessWidget {
),
),
),
if (groupType == GroupType.URLTest)
if (groupType.isURLTestOrFallback)
Selector<Config, String>(
selector: (_, config) {
final selectedProxyName =

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
@@ -37,7 +38,10 @@ class _RequestsFragmentState extends State<RequestsFragment> {
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final requests = appState.requests;
final maxLength = Platform.isAndroid ? 1000 : 60;
final requests = appState.requests.safeSublist(
appState.requests.length - maxLength,
);
if (!const ListEquality<Connection>().equals(
requestsNotifier.value.connections,
requests,

View File

@@ -70,7 +70,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
final isDisclaimerAccepted =
await globalState.appController.showDisclaimer();
if (!isDisclaimerAccepted) {
system.exit();
globalState.appController.handleExit();
}
},
),

View File

@@ -123,7 +123,7 @@
"tabAnimation": "Tab animation",
"tabAnimationDesc": "When enabled, the home tab will add a toggle animation",
"desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.",
"startVpn": "Staring VPN...",
"startVpn": "Starting VPN...",
"stopVpn": "Stopping VPN...",
"discovery": "Discovery a new version",
"compatible": "Compatibility mode",
@@ -304,5 +304,9 @@
"hotkeyConflict": "Hotkey conflict",
"remove": "Remove",
"noHotKey": "No HotKey",
"dnsResetTip": "Make sure to reset the DNS"
"dnsResetTip": "Make sure to reset the DNS",
"noNetwork": "No network",
"ipv6InboundDesc": "Allow IPv6 inbound",
"exportLogs": "Export logs",
"exportSuccess": "Export Success"
}

View File

@@ -304,5 +304,9 @@
"hotkeyConflict": "快捷键冲突",
"remove": "移除",
"noHotKey": "暂无快捷键",
"dnsResetTip": "确定重置DNS"
"dnsResetTip": "确定重置DNS",
"noNetwork": "无网络",
"ipv6InboundDesc": "允许IPv6入站",
"exportLogs": "导出日志",
"exportSuccess": "导出成功"
}

View File

@@ -163,6 +163,8 @@ class MessageLookup extends MessageLookupByLibrary {
"expand": MessageLookupByLibrary.simpleMessage("Standard"),
"expirationTime":
MessageLookupByLibrary.simpleMessage("Expiration time"),
"exportLogs": MessageLookupByLibrary.simpleMessage("Export logs"),
"exportSuccess": MessageLookupByLibrary.simpleMessage("Export Success"),
"externalController":
MessageLookupByLibrary.simpleMessage("ExternalController"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
@@ -219,6 +221,8 @@ class MessageLookup extends MessageLookupByLibrary {
"ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
"When turned on it will be able to receive IPv6 traffic"),
"ipv6InboundDesc":
MessageLookupByLibrary.simpleMessage("Allow IPv6 inbound"),
"just": MessageLookupByLibrary.simpleMessage("Just"),
"keepAliveIntervalDesc":
MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"),
@@ -269,6 +273,7 @@ class MessageLookup extends MessageLookupByLibrary {
"noHotKey": MessageLookupByLibrary.simpleMessage("No HotKey"),
"noInfo": MessageLookupByLibrary.simpleMessage("No info"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"),
"noNetwork": MessageLookupByLibrary.simpleMessage("No network"),
"noProxy": MessageLookupByLibrary.simpleMessage("No proxy"),
"noProxyDesc": MessageLookupByLibrary.simpleMessage(
"Please create a profile or add a valid profile"),
@@ -395,7 +400,7 @@ class MessageLookup extends MessageLookupByLibrary {
"source": MessageLookupByLibrary.simpleMessage("Source"),
"standard": MessageLookupByLibrary.simpleMessage("Standard"),
"start": MessageLookupByLibrary.simpleMessage("Start"),
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
"startVpn": MessageLookupByLibrary.simpleMessage("Starting VPN..."),
"status": MessageLookupByLibrary.simpleMessage("Status"),
"statusDesc": MessageLookupByLibrary.simpleMessage(
"System DNS will be used when turned off"),

View File

@@ -133,6 +133,8 @@ class MessageLookup extends MessageLookupByLibrary {
"exit": MessageLookupByLibrary.simpleMessage("退出"),
"expand": MessageLookupByLibrary.simpleMessage("标准"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc":
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
@@ -174,6 +176,7 @@ class MessageLookup extends MessageLookupByLibrary {
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"keepAliveIntervalDesc":
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
@@ -214,6 +217,7 @@ class MessageLookup extends MessageLookupByLibrary {
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"),
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
"noProxyDesc":
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),

View File

@@ -1290,10 +1290,10 @@ class AppLocalizations {
);
}
/// `Staring VPN...`
/// `Starting VPN...`
String get startVpn {
return Intl.message(
'Staring VPN...',
'Starting VPN...',
name: 'startVpn',
desc: '',
args: [],
@@ -3109,6 +3109,46 @@ class AppLocalizations {
args: [],
);
}
/// `No network`
String get noNetwork {
return Intl.message(
'No network',
name: 'noNetwork',
desc: '',
args: [],
);
}
/// `Allow IPv6 inbound`
String get ipv6InboundDesc {
return Intl.message(
'Allow IPv6 inbound',
name: 'ipv6InboundDesc',
desc: '',
args: [],
);
}
/// `Export logs`
String get exportLogs {
return Intl.message(
'Export logs',
name: 'exportLogs',
desc: '',
args: [],
);
}
/// `Export Success`
String get exportSuccess {
return Intl.message(
'Export Success',
name: 'exportSuccess',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/plugins/app.dart';

View File

@@ -18,6 +18,7 @@ class AppStateManager extends StatefulWidget {
class _AppStateManagerState extends State<AppStateManager>
with WidgetsBindingObserver {
_updateNavigationsContainer(Widget child) {
return Selector2<AppState, Config, UpdateNavigationsSelector>(
selector: (_, appState, config) {

View File

@@ -19,9 +19,9 @@ class ClashManager extends StatefulWidget {
State<ClashManager> createState() => _ClashContainerState();
}
class _ClashContainerState extends State<ClashManager>
with AppMessageListener {
class _ClashContainerState extends State<ClashManager> with AppMessageListener {
Function? updateClashConfigDebounce;
Function? updateDelayDebounce;
Widget _updateContainer(Widget child) {
return Selector2<Config, ClashConfig, ClashConfigState>(
@@ -66,6 +66,7 @@ class _ClashContainerState extends State<ClashManager>
selector: (_, config, clashConfig) => CoreState(
accessControl: config.isAccessControl ? config.accessControl : null,
enable: config.vpnProps.enable,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
systemProxy: config.vpnProps.systemProxy,
mixedPort: clashConfig.mixedPort,
@@ -132,7 +133,11 @@ class _ClashContainerState extends State<ClashManager>
final appController = globalState.appController;
appController.setDelay(delay);
super.onDelay(delay);
await globalState.appController.updateGroupDebounce();
updateDelayDebounce ??= debounce(() async {
await appController.updateGroupDebounce();
await appController.addCheckIpNumDebounce();
});
updateDelayDebounce!();
}
@override
@@ -145,6 +150,12 @@ class _ClashContainerState extends State<ClashManager>
super.onLog(log);
}
@override
void onStarted(String runTime) {
super.onStarted(runTime);
globalState.appController.applyProfileDebounce();
}
@override
void onRequest(Connection connection) async {
globalState.appController.appState.addRequest(connection);
@@ -159,7 +170,7 @@ class _ClashContainerState extends State<ClashManager>
providerName,
),
);
appController.addCheckIpNumDebounce();
// appController.addCheckIpNumDebounce();
super.onLoaded(providerName);
}
}

View File

@@ -151,10 +151,8 @@ class AppState with ChangeNotifier {
addRequest(Connection value) {
_requests = List.from(_requests)..add(value);
final maxLength = Platform.isAndroid ? 1000 : 60;
if (_requests.length > maxLength) {
_requests = _requests.sublist(_requests.length - maxLength);
}
const maxLength = 1000;
_requests = _requests.safeSublist(_requests.length - maxLength);
notifyListeners();
}
@@ -354,10 +352,8 @@ class AppFlowingState with ChangeNotifier {
addLog(Log log) {
_logs = List.from(_logs)..add(log);
final maxLength = Platform.isAndroid ? 1000 : 60;
if (_logs.length > maxLength) {
_logs = _logs.sublist(_logs.length - maxLength);
}
const maxLength = 1000;
_logs = _logs.safeSublist(_logs.length - maxLength);
notifyListeners();
}
@@ -373,9 +369,7 @@ class AppFlowingState with ChangeNotifier {
addTraffic(Traffic traffic) {
_traffics = List.from(_traffics)..add(traffic);
const maxLength = 60;
if (_traffics.length > maxLength) {
_traffics = _traffics.sublist(_traffics.length - maxLength);
}
_traffics = _traffics.safeSublist(_traffics.length - maxLength);
notifyListeners();
}

View File

@@ -303,7 +303,7 @@ extension GroupExt on Group {
String get realNow => now ?? "";
String getCurrentSelectedName(String proxyName) {
if (type == GroupType.URLTest) {
if (type.isURLTestOrFallback) {
return realNow.isNotEmpty ? realNow : proxyName;
}
return proxyName.isNotEmpty ? proxyName : realNow;

View File

@@ -42,6 +42,7 @@ class CoreState with _$CoreState {
required bool allowBypass,
required bool systemProxy,
required int mixedPort,
required bool ipv6,
required bool onlyProxy,
}) = _CoreState;
@@ -77,7 +78,8 @@ class WindowProps with _$WindowProps {
class VpnProps with _$VpnProps {
const factory VpnProps({
@Default(true) bool enable,
@Default(false) bool systemProxy,
@Default(true) bool systemProxy,
@Default(false) bool ipv6,
@Default(true) bool allowBypass,
}) = _VpnProps;

View File

@@ -32,9 +32,9 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
'mmdb':
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb',
'asn':
'https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb',
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb',
'geoip':
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat',
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat',
'geosite':
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat'
}

View File

@@ -274,6 +274,7 @@ mixin _$CoreState {
bool get allowBypass => throw _privateConstructorUsedError;
bool get systemProxy => throw _privateConstructorUsedError;
int get mixedPort => throw _privateConstructorUsedError;
bool get ipv6 => throw _privateConstructorUsedError;
bool get onlyProxy => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -294,6 +295,7 @@ abstract class $CoreStateCopyWith<$Res> {
bool allowBypass,
bool systemProxy,
int mixedPort,
bool ipv6,
bool onlyProxy});
$AccessControlCopyWith<$Res>? get accessControl;
@@ -318,6 +320,7 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
Object? allowBypass = null,
Object? systemProxy = null,
Object? mixedPort = null,
Object? ipv6 = null,
Object? onlyProxy = null,
}) {
return _then(_value.copyWith(
@@ -345,6 +348,10 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
? _value.mixedPort
: mixedPort // ignore: cast_nullable_to_non_nullable
as int,
ipv6: null == ipv6
? _value.ipv6
: ipv6 // ignore: cast_nullable_to_non_nullable
as bool,
onlyProxy: null == onlyProxy
? _value.onlyProxy
: onlyProxy // ignore: cast_nullable_to_non_nullable
@@ -380,6 +387,7 @@ abstract class _$$CoreStateImplCopyWith<$Res>
bool allowBypass,
bool systemProxy,
int mixedPort,
bool ipv6,
bool onlyProxy});
@override
@@ -403,6 +411,7 @@ class __$$CoreStateImplCopyWithImpl<$Res>
Object? allowBypass = null,
Object? systemProxy = null,
Object? mixedPort = null,
Object? ipv6 = null,
Object? onlyProxy = null,
}) {
return _then(_$CoreStateImpl(
@@ -430,6 +439,10 @@ class __$$CoreStateImplCopyWithImpl<$Res>
? _value.mixedPort
: mixedPort // ignore: cast_nullable_to_non_nullable
as int,
ipv6: null == ipv6
? _value.ipv6
: ipv6 // ignore: cast_nullable_to_non_nullable
as bool,
onlyProxy: null == onlyProxy
? _value.onlyProxy
: onlyProxy // ignore: cast_nullable_to_non_nullable
@@ -448,6 +461,7 @@ class _$CoreStateImpl implements _CoreState {
required this.allowBypass,
required this.systemProxy,
required this.mixedPort,
required this.ipv6,
required this.onlyProxy});
factory _$CoreStateImpl.fromJson(Map<String, dynamic> json) =>
@@ -466,11 +480,13 @@ class _$CoreStateImpl implements _CoreState {
@override
final int mixedPort;
@override
final bool ipv6;
@override
final bool onlyProxy;
@override
String toString() {
return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, enable: $enable, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)';
return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, enable: $enable, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, ipv6: $ipv6, onlyProxy: $onlyProxy)';
}
@override
@@ -489,6 +505,7 @@ class _$CoreStateImpl implements _CoreState {
other.systemProxy == systemProxy) &&
(identical(other.mixedPort, mixedPort) ||
other.mixedPort == mixedPort) &&
(identical(other.ipv6, ipv6) || other.ipv6 == ipv6) &&
(identical(other.onlyProxy, onlyProxy) ||
other.onlyProxy == onlyProxy));
}
@@ -503,6 +520,7 @@ class _$CoreStateImpl implements _CoreState {
allowBypass,
systemProxy,
mixedPort,
ipv6,
onlyProxy);
@JsonKey(ignore: true)
@@ -527,6 +545,7 @@ abstract class _CoreState implements CoreState {
required final bool allowBypass,
required final bool systemProxy,
required final int mixedPort,
required final bool ipv6,
required final bool onlyProxy}) = _$CoreStateImpl;
factory _CoreState.fromJson(Map<String, dynamic> json) =
@@ -545,6 +564,8 @@ abstract class _CoreState implements CoreState {
@override
int get mixedPort;
@override
bool get ipv6;
@override
bool get onlyProxy;
@override
@JsonKey(ignore: true)
@@ -934,6 +955,7 @@ VpnProps _$VpnPropsFromJson(Map<String, dynamic> json) {
mixin _$VpnProps {
bool get enable => throw _privateConstructorUsedError;
bool get systemProxy => throw _privateConstructorUsedError;
bool get ipv6 => throw _privateConstructorUsedError;
bool get allowBypass => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -947,7 +969,7 @@ abstract class $VpnPropsCopyWith<$Res> {
factory $VpnPropsCopyWith(VpnProps value, $Res Function(VpnProps) then) =
_$VpnPropsCopyWithImpl<$Res, VpnProps>;
@useResult
$Res call({bool enable, bool systemProxy, bool allowBypass});
$Res call({bool enable, bool systemProxy, bool ipv6, bool allowBypass});
}
/// @nodoc
@@ -965,6 +987,7 @@ class _$VpnPropsCopyWithImpl<$Res, $Val extends VpnProps>
$Res call({
Object? enable = null,
Object? systemProxy = null,
Object? ipv6 = null,
Object? allowBypass = null,
}) {
return _then(_value.copyWith(
@@ -976,6 +999,10 @@ class _$VpnPropsCopyWithImpl<$Res, $Val extends VpnProps>
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool,
ipv6: null == ipv6
? _value.ipv6
: ipv6 // ignore: cast_nullable_to_non_nullable
as bool,
allowBypass: null == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
@@ -992,7 +1019,7 @@ abstract class _$$VpnPropsImplCopyWith<$Res>
__$$VpnPropsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool enable, bool systemProxy, bool allowBypass});
$Res call({bool enable, bool systemProxy, bool ipv6, bool allowBypass});
}
/// @nodoc
@@ -1008,6 +1035,7 @@ class __$$VpnPropsImplCopyWithImpl<$Res>
$Res call({
Object? enable = null,
Object? systemProxy = null,
Object? ipv6 = null,
Object? allowBypass = null,
}) {
return _then(_$VpnPropsImpl(
@@ -1019,6 +1047,10 @@ class __$$VpnPropsImplCopyWithImpl<$Res>
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool,
ipv6: null == ipv6
? _value.ipv6
: ipv6 // ignore: cast_nullable_to_non_nullable
as bool,
allowBypass: null == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
@@ -1031,7 +1063,10 @@ class __$$VpnPropsImplCopyWithImpl<$Res>
@JsonSerializable()
class _$VpnPropsImpl implements _VpnProps {
const _$VpnPropsImpl(
{this.enable = true, this.systemProxy = false, this.allowBypass = true});
{this.enable = true,
this.systemProxy = true,
this.ipv6 = false,
this.allowBypass = true});
factory _$VpnPropsImpl.fromJson(Map<String, dynamic> json) =>
_$$VpnPropsImplFromJson(json);
@@ -1044,11 +1079,14 @@ class _$VpnPropsImpl implements _VpnProps {
final bool systemProxy;
@override
@JsonKey()
final bool ipv6;
@override
@JsonKey()
final bool allowBypass;
@override
String toString() {
return 'VpnProps(enable: $enable, systemProxy: $systemProxy, allowBypass: $allowBypass)';
return 'VpnProps(enable: $enable, systemProxy: $systemProxy, ipv6: $ipv6, allowBypass: $allowBypass)';
}
@override
@@ -1059,6 +1097,7 @@ class _$VpnPropsImpl implements _VpnProps {
(identical(other.enable, enable) || other.enable == enable) &&
(identical(other.systemProxy, systemProxy) ||
other.systemProxy == systemProxy) &&
(identical(other.ipv6, ipv6) || other.ipv6 == ipv6) &&
(identical(other.allowBypass, allowBypass) ||
other.allowBypass == allowBypass));
}
@@ -1066,7 +1105,7 @@ class _$VpnPropsImpl implements _VpnProps {
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, enable, systemProxy, allowBypass);
Object.hash(runtimeType, enable, systemProxy, ipv6, allowBypass);
@JsonKey(ignore: true)
@override
@@ -1086,6 +1125,7 @@ abstract class _VpnProps implements VpnProps {
const factory _VpnProps(
{final bool enable,
final bool systemProxy,
final bool ipv6,
final bool allowBypass}) = _$VpnPropsImpl;
factory _VpnProps.fromJson(Map<String, dynamic> json) =
@@ -1096,6 +1136,8 @@ abstract class _VpnProps implements VpnProps {
@override
bool get systemProxy;
@override
bool get ipv6;
@override
bool get allowBypass;
@override
@JsonKey(ignore: true)

View File

@@ -176,6 +176,7 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map<String, dynamic> json) =>
allowBypass: json['allowBypass'] as bool,
systemProxy: json['systemProxy'] as bool,
mixedPort: (json['mixedPort'] as num).toInt(),
ipv6: json['ipv6'] as bool,
onlyProxy: json['onlyProxy'] as bool,
);
@@ -187,6 +188,7 @@ Map<String, dynamic> _$$CoreStateImplToJson(_$CoreStateImpl instance) =>
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
'mixedPort': instance.mixedPort,
'ipv6': instance.ipv6,
'onlyProxy': instance.onlyProxy,
};
@@ -224,7 +226,8 @@ Map<String, dynamic> _$$WindowPropsImplToJson(_$WindowPropsImpl instance) =>
_$VpnPropsImpl _$$VpnPropsImplFromJson(Map<String, dynamic> json) =>
_$VpnPropsImpl(
enable: json['enable'] as bool? ?? true,
systemProxy: json['systemProxy'] as bool? ?? false,
systemProxy: json['systemProxy'] as bool? ?? true,
ipv6: json['ipv6'] as bool? ?? false,
allowBypass: json['allowBypass'] as bool? ?? true,
);
@@ -232,6 +235,7 @@ Map<String, dynamic> _$$VpnPropsImplToJson(_$VpnPropsImpl instance) =>
<String, dynamic>{
'enable': instance.enable,
'systemProxy': instance.systemProxy,
'ipv6': instance.ipv6,
'allowBypass': instance.allowBypass,
};

View File

@@ -90,7 +90,7 @@ class GlobalState {
startTime = clashCore.getRunTime();
}
handleStop() async {
Future handleStop() async {
clashCore.stop();
if (Platform.isAndroid) {
clashCore.stopTun();
@@ -133,6 +133,7 @@ class GlobalState {
CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
systemProxy: config.vpnProps.systemProxy,
mixedPort: clashConfig.mixedPort,

View File

@@ -149,7 +149,6 @@ class UpdatePage<T> extends StatelessWidget {
final Widget Function(T item)? subtitleBuilder;
final Function(T item) onAdd;
final Function(T item) onRemove;
final bool isMap;
const UpdatePage({
super.key,
@@ -158,10 +157,37 @@ class UpdatePage<T> extends StatelessWidget {
required this.titleBuilder,
required this.onRemove,
required this.onAdd,
this.isMap = false,
this.subtitleBuilder,
});
bool get isMap => items is Iterable<MapEntry>;
_handleEdit(T item) async {
if (isMap) {
item as MapEntry<String, String>;
final value = await globalState.showCommonDialog<T>(
child: AddDialog(
defaultKey: item.key,
defaultValue: item.value,
title: title,
),
);
if (value == null) return;
onAdd(value);
} else {
item as String;
final value = await globalState.showCommonDialog<T>(
child: AddDialog(
defaultKey: null,
defaultValue: item,
title: title,
),
);
if (value == null) return;
onAdd(value);
}
}
@override
Widget build(BuildContext context) {
return FloatLayout(
@@ -170,7 +196,8 @@ class UpdatePage<T> extends StatelessWidget {
onPressed: () async {
final value = await globalState.showCommonDialog<T>(
child: AddDialog(
isMap: isMap,
defaultKey: isMap ? "" : null,
defaultValue: "",
title: title,
),
);
@@ -202,7 +229,9 @@ class UpdatePage<T> extends StatelessWidget {
},
),
),
onPressed: () {},
onPressed: () {
_handleEdit(e);
},
),
);
},
@@ -213,12 +242,14 @@ class UpdatePage<T> extends StatelessWidget {
class AddDialog extends StatefulWidget {
final String title;
final bool isMap;
final String? defaultKey;
final String defaultValue;
const AddDialog({
super.key,
required this.title,
required this.isMap,
this.defaultKey,
required this.defaultValue,
});
@override
@@ -233,13 +264,19 @@ class _AddDialogState extends State<AddDialog> {
@override
void initState() {
super.initState();
keyController = TextEditingController();
valueController = TextEditingController();
keyController = TextEditingController(
text: widget.defaultKey,
);
valueController = TextEditingController(
text: widget.defaultValue,
);
}
bool get hasKey => widget.defaultKey != null;
_submit() {
if (!_formKey.currentState!.validate()) return;
if (widget.isMap) {
if (hasKey) {
Navigator.of(context).pop<MapEntry<String, String>>(
MapEntry(
keyController.text,
@@ -264,7 +301,7 @@ class _AddDialogState extends State<AddDialog> {
child: Wrap(
runSpacing: 16,
children: [
if (widget.isMap)
if (hasKey)
TextFormField(
maxLines: 2,
minLines: 1,