Support proxies search Support svg display Optimize config persistence Add some scenes auto close connections Update core Optimize more details
398 lines
12 KiB
Dart
398 lines
12 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
import 'dart:ui';
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:lpinyin/lpinyin.dart';
|
|
|
|
class Utils {
|
|
Color? getDelayColor(int? delay) {
|
|
if (delay == null) return null;
|
|
if (delay < 0) return Colors.red;
|
|
if (delay < 600) return Colors.green;
|
|
return const Color(0xFFC57F0A);
|
|
}
|
|
|
|
String get id {
|
|
final timestamp = DateTime.now().microsecondsSinceEpoch;
|
|
final random = Random();
|
|
final randomStr =
|
|
String.fromCharCodes(List.generate(8, (_) => random.nextInt(26) + 97));
|
|
return "$timestamp$randomStr";
|
|
}
|
|
|
|
String getDateStringLast2(int value) {
|
|
var valueRaw = "0$value";
|
|
return valueRaw.substring(
|
|
valueRaw.length - 2,
|
|
);
|
|
}
|
|
|
|
String generateRandomString({int minLength = 10, int maxLength = 100}) {
|
|
const latinChars =
|
|
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
final random = Random();
|
|
|
|
int length = minLength + random.nextInt(maxLength - minLength + 1);
|
|
|
|
String result = '';
|
|
for (int i = 0; i < length; i++) {
|
|
if (random.nextBool()) {
|
|
result +=
|
|
String.fromCharCode(0x4E00 + random.nextInt(0x9FA5 - 0x4E00 + 1));
|
|
} else {
|
|
result += latinChars[random.nextInt(latinChars.length)];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
String get uuidV4 {
|
|
final Random random = Random();
|
|
final bytes = List.generate(16, (_) => random.nextInt(256));
|
|
|
|
bytes[6] = (bytes[6] & 0x0F) | 0x40;
|
|
bytes[8] = (bytes[8] & 0x3F) | 0x80;
|
|
|
|
final hex =
|
|
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
|
|
|
return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}';
|
|
}
|
|
|
|
String getTimeDifference(DateTime dateTime) {
|
|
var currentDateTime = DateTime.now();
|
|
var difference = currentDateTime.difference(dateTime);
|
|
var inHours = difference.inHours;
|
|
var inMinutes = difference.inMinutes;
|
|
var inSeconds = difference.inSeconds;
|
|
|
|
return "${getDateStringLast2(inHours)}:${getDateStringLast2(inMinutes)}:${getDateStringLast2(inSeconds)}";
|
|
}
|
|
|
|
String getTimeText(int? timeStamp) {
|
|
if (timeStamp == null) {
|
|
return '00:00:00';
|
|
}
|
|
final diff = timeStamp / 1000;
|
|
final inHours = (diff / 3600).floor();
|
|
if (inHours > 99) {
|
|
return "99:59:59";
|
|
}
|
|
final inMinutes = (diff / 60 % 60).floor();
|
|
final inSeconds = (diff % 60).floor();
|
|
|
|
return "${getDateStringLast2(inHours)}:${getDateStringLast2(inMinutes)}:${getDateStringLast2(inSeconds)}";
|
|
}
|
|
|
|
Locale? getLocaleForString(String? localString) {
|
|
if (localString == null) return null;
|
|
var localSplit = localString.split("_");
|
|
if (localSplit.length == 1) {
|
|
return Locale(localSplit[0]);
|
|
}
|
|
if (localSplit.length == 2) {
|
|
return Locale(localSplit[0], localSplit[1]);
|
|
}
|
|
if (localSplit.length == 3) {
|
|
return Locale.fromSubtags(
|
|
languageCode: localSplit[0],
|
|
scriptCode: localSplit[1],
|
|
countryCode: localSplit[2]);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
int sortByChar(String a, String b) {
|
|
if (a.isEmpty && b.isEmpty) {
|
|
return 0;
|
|
}
|
|
if (a.isEmpty) {
|
|
return -1;
|
|
}
|
|
if (b.isEmpty) {
|
|
return 1;
|
|
}
|
|
final charA = a[0];
|
|
final charB = b[0];
|
|
|
|
if (charA == charB) {
|
|
return sortByChar(a.substring(1), b.substring(1));
|
|
} else {
|
|
return charA.compareToLower(charB);
|
|
}
|
|
}
|
|
|
|
String getOverwriteLabel(String label) {
|
|
final reg = RegExp(r'\((\d+)\)$');
|
|
final matches = reg.allMatches(label);
|
|
if (matches.isNotEmpty) {
|
|
final match = matches.last;
|
|
final number = int.parse(match[1] ?? '0') + 1;
|
|
return label.replaceFirst(reg, '($number)', label.length - 3 - 1);
|
|
} else {
|
|
return "$label(1)";
|
|
}
|
|
}
|
|
|
|
String getTrayIconPath({
|
|
required Brightness brightness,
|
|
}) {
|
|
if (Platform.isMacOS) {
|
|
return "assets/images/icon_white.png";
|
|
}
|
|
final suffix = Platform.isWindows ? "ico" : "png";
|
|
return "assets/images/icon.$suffix";
|
|
// return switch (brightness) {
|
|
// Brightness.dark => "assets/images/icon_white.$suffix",
|
|
// Brightness.light => "assets/images/icon_black.$suffix",
|
|
// };
|
|
}
|
|
|
|
int compareVersions(String version1, String version2) {
|
|
List<String> v1 = version1.split('+')[0].split('.');
|
|
List<String> v2 = version2.split('+')[0].split('.');
|
|
int major1 = int.parse(v1[0]);
|
|
int major2 = int.parse(v2[0]);
|
|
if (major1 != major2) {
|
|
return major1.compareTo(major2);
|
|
}
|
|
int minor1 = v1.length > 1 ? int.parse(v1[1]) : 0;
|
|
int minor2 = v2.length > 1 ? int.parse(v2[1]) : 0;
|
|
if (minor1 != minor2) {
|
|
return minor1.compareTo(minor2);
|
|
}
|
|
int patch1 = v1.length > 2 ? int.parse(v1[2]) : 0;
|
|
int patch2 = v2.length > 2 ? int.parse(v2[2]) : 0;
|
|
if (patch1 != patch2) {
|
|
return patch1.compareTo(patch2);
|
|
}
|
|
int build1 = version1.contains('+') ? int.parse(version1.split('+')[1]) : 0;
|
|
int build2 = version2.contains('+') ? int.parse(version2.split('+')[1]) : 0;
|
|
return build1.compareTo(build2);
|
|
}
|
|
|
|
String getPinyin(String value) {
|
|
return value.isNotEmpty
|
|
? PinyinHelper.getFirstWordPinyin(value.substring(0, 1))
|
|
: "";
|
|
}
|
|
|
|
String? getFileNameForDisposition(String? disposition) {
|
|
if (disposition == null) return null;
|
|
final parseValue = HeaderValue.parse(disposition);
|
|
final parameters = parseValue.parameters;
|
|
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];
|
|
}
|
|
|
|
FlutterView getScreen() {
|
|
return WidgetsBinding.instance.platformDispatcher.views.first;
|
|
}
|
|
|
|
List<String> parseReleaseBody(String? body) {
|
|
if (body == null) return [];
|
|
const pattern = r'- \s*(.*)';
|
|
final regex = RegExp(pattern);
|
|
return regex
|
|
.allMatches(body)
|
|
.map((match) => match.group(1) ?? '')
|
|
.where((item) => item.isNotEmpty)
|
|
.toList();
|
|
}
|
|
|
|
ViewMode getViewMode(double viewWidth) {
|
|
if (viewWidth <= maxMobileWidth) return ViewMode.mobile;
|
|
if (viewWidth <= maxLaptopWidth) return ViewMode.laptop;
|
|
return ViewMode.desktop;
|
|
}
|
|
|
|
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
|
|
final columns = max((viewWidth / 300).ceil(), 2);
|
|
return switch (proxiesLayout) {
|
|
ProxiesLayout.tight => columns + 1,
|
|
ProxiesLayout.standard => columns,
|
|
ProxiesLayout.loose => columns - 1,
|
|
};
|
|
}
|
|
|
|
int getProfilesColumns(double viewWidth) {
|
|
return max((viewWidth / 320).floor(), 1);
|
|
}
|
|
|
|
final _indexPrimary = [
|
|
50,
|
|
100,
|
|
200,
|
|
300,
|
|
400,
|
|
500,
|
|
600,
|
|
700,
|
|
800,
|
|
850,
|
|
900,
|
|
];
|
|
|
|
_createPrimarySwatch(Color color) {
|
|
final Map<int, Color> swatch = <int, Color>{};
|
|
final int a = color.alpha8bit;
|
|
final int r = color.red8bit;
|
|
final int g = color.green8bit;
|
|
final int b = color.blue8bit;
|
|
for (final int strength in _indexPrimary) {
|
|
final double ds = 0.5 - strength / 1000;
|
|
swatch[strength] = Color.fromARGB(
|
|
a,
|
|
r + ((ds < 0 ? r : (255 - r)) * ds).round(),
|
|
g + ((ds < 0 ? g : (255 - g)) * ds).round(),
|
|
b + ((ds < 0 ? b : (255 - b)) * ds).round(),
|
|
);
|
|
}
|
|
swatch[50] = swatch[50]!.lighten(18);
|
|
swatch[100] = swatch[100]!.lighten(16);
|
|
swatch[200] = swatch[200]!.lighten(14);
|
|
swatch[300] = swatch[300]!.lighten(10);
|
|
swatch[400] = swatch[400]!.lighten(6);
|
|
swatch[700] = swatch[700]!.darken(2);
|
|
swatch[800] = swatch[800]!.darken(3);
|
|
swatch[900] = swatch[900]!.darken(4);
|
|
return MaterialColor(color.value32bit, swatch);
|
|
}
|
|
|
|
List<Color> getMaterialColorShades(Color color) {
|
|
final swatch = _createPrimarySwatch(color);
|
|
return <Color>[
|
|
if (swatch[50] != null) swatch[50]!,
|
|
if (swatch[100] != null) swatch[100]!,
|
|
if (swatch[200] != null) swatch[200]!,
|
|
if (swatch[300] != null) swatch[300]!,
|
|
if (swatch[400] != null) swatch[400]!,
|
|
if (swatch[500] != null) swatch[500]!,
|
|
if (swatch[600] != null) swatch[600]!,
|
|
if (swatch[700] != null) swatch[700]!,
|
|
if (swatch[800] != null) swatch[800]!,
|
|
if (swatch[850] != null) swatch[850]!,
|
|
if (swatch[900] != null) swatch[900]!,
|
|
];
|
|
}
|
|
|
|
String getBackupFileName() {
|
|
return "${appName}_backup_${DateTime.now().show}.zip";
|
|
}
|
|
|
|
String get logFile {
|
|
return "${appName}_${DateTime.now().show}.log";
|
|
}
|
|
|
|
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 "";
|
|
}
|
|
|
|
SingleActivator controlSingleActivator(LogicalKeyboardKey trigger) {
|
|
final control = Platform.isMacOS ? false : true;
|
|
return SingleActivator(
|
|
trigger,
|
|
control: control,
|
|
meta: !control,
|
|
);
|
|
}
|
|
|
|
// dynamic convertYamlNode(dynamic node) {
|
|
// if (node is YamlMap) {
|
|
// final map = <String, dynamic>{};
|
|
// YamlNode? mergeKeyNode;
|
|
// for (final entry in node.nodes.entries) {
|
|
// if (entry.key is YamlScalar &&
|
|
// (entry.key as YamlScalar).value == '<<') {
|
|
// mergeKeyNode = entry.value;
|
|
// break;
|
|
// }
|
|
// }
|
|
// if (mergeKeyNode != null) {
|
|
// final mergeValue = mergeKeyNode.value;
|
|
// if (mergeValue is YamlMap) {
|
|
// map.addAll(convertYamlNode(mergeValue) as Map<String, dynamic>);
|
|
// } else if (mergeValue is YamlList) {
|
|
// for (final node in mergeValue.nodes) {
|
|
// if (node.value is YamlMap) {
|
|
// map.addAll(convertYamlNode(node.value) as Map<String, dynamic>);
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// node.nodes.forEach((key, value) {
|
|
// String stringKey;
|
|
// if (key is YamlScalar) {
|
|
// stringKey = key.value.toString();
|
|
// } else {
|
|
// stringKey = key.toString();
|
|
// }
|
|
// map[stringKey] = convertYamlNode(value.value);
|
|
// });
|
|
// return map;
|
|
// } else if (node is YamlList) {
|
|
// final list = <dynamic>[];
|
|
// for (final item in node.nodes) {
|
|
// list.add(convertYamlNode(item.value));
|
|
// }
|
|
// return list;
|
|
// } else if (node is YamlScalar) {
|
|
// return node.value;
|
|
// }
|
|
// return node;
|
|
// }
|
|
|
|
FutureOr<T> handleWatch<T>(Function function) async {
|
|
if (kDebugMode) {
|
|
final stopwatch = Stopwatch()..start();
|
|
final res = await function();
|
|
stopwatch.stop();
|
|
commonPrint.log('耗时:${stopwatch.elapsedMilliseconds} ms');
|
|
return res;
|
|
}
|
|
return await function();
|
|
}
|
|
}
|
|
|
|
final utils = Utils();
|