Compare commits

...

2 Commits

Author SHA1 Message Date
chen08209
c6266b7917 Add windows storage corruption detection
Fix core crash caused by windows resource manager restart

Optimize logs, requests, access to pages

Fix macos bypass domain issues
2025-02-09 16:23:40 +08:00
chen08209
6c27f2e2f1 Update changelog 2025-02-03 13:28:20 +00:00
66 changed files with 3078 additions and 3573 deletions

View File

@@ -1,3 +1,9 @@
## v0.8.74
- Fix some issues
- Update changelog
## v0.8.73
- Update popup menu

View File

@@ -4,5 +4,5 @@ data class Package(
val packageName: String,
val label: String,
val isSystem: Boolean,
val firstInstallTime: Long,
val lastUpdateTime: Long,
)

View File

@@ -37,7 +37,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.lang.ref.WeakReference
@@ -302,7 +301,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
firstInstallTime = it.firstInstallTime
lastUpdateTime = it.lastUpdateTime
)
}?.let { packages.addAll(it) }
return packages

View File

@@ -33,7 +33,7 @@ class ClashCore {
}
static Future<void> initGeo() async {
final homePath = await appPath.getHomeDirPath();
final homePath = await appPath.homeDirPath;
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
@@ -68,7 +68,7 @@ class ClashCore {
required Config config,
}) async {
await initGeo();
final homeDirPath = await appPath.getHomeDirPath();
final homeDirPath = await appPath.homeDirPath;
return await clashInterface.init(homeDirPath);
}

View File

@@ -268,9 +268,9 @@ abstract class ClashHandlerInterface with ClashInterface {
@override
Future<String> updateGeoData(UpdateGeoDataParams params) {
return invoke<String>(
method: ActionMethod.updateGeoData,
data: json.encode(params),
);
method: ActionMethod.updateGeoData,
data: json.encode(params),
timeout: Duration(minutes: 1));
}
@override
@@ -292,6 +292,7 @@ abstract class ClashHandlerInterface with ClashInterface {
return invoke<String>(
method: ActionMethod.updateExternalProvider,
data: providerName,
timeout: Duration(minutes: 1),
);
}

View File

@@ -34,4 +34,5 @@ export 'text.dart';
export 'tray.dart';
export 'window.dart';
export 'windows.dart';
export 'render.dart';
export 'render.dart';
export 'view.dart';

View File

@@ -22,4 +22,23 @@ extension BuildContextExtension on BuildContext {
ColorScheme get colorScheme => Theme.of(this).colorScheme;
TextTheme get textTheme => Theme.of(this).textTheme;
T? findLastStateOfType<T extends State>() {
T? state;
visitor(Element element) {
if(!element.mounted){
return;
}
if(element is StatefulElement){
if (element.state is T) {
state = element.state as T;
}
}
element.visitChildren(visitor);
}
visitor(this as Element);
return state;
}
}

View File

@@ -1,7 +1,7 @@
import 'dart:async';
class Debouncer {
Map<dynamic, Timer> operators = {};
final Map<dynamic, Timer> _operations = {};
call(
dynamic tag,
@@ -9,14 +9,15 @@ class Debouncer {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
}) {
final timer = operators[tag];
final timer = _operations[tag];
if (timer != null) {
timer.cancel();
}
operators[tag] = Timer(
_operations[tag] = Timer(
duration,
() {
operators.remove(tag);
_operations[tag]?.cancel();
_operations.remove(tag);
Function.apply(
func,
args,
@@ -26,8 +27,43 @@ class Debouncer {
}
cancel(dynamic tag) {
operators[tag]?.cancel();
_operations[tag]?.cancel();
}
}
class Throttler {
final Map<dynamic, Timer> _operations = {};
call(
String tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
}) {
final timer = _operations[tag];
if (timer != null) {
return true;
}
_operations[tag] = Timer(
duration,
() {
_operations[tag]?.cancel();
_operations.remove(tag);
Function.apply(
func,
args,
);
},
);
return false;
}
cancel(dynamic tag) {
_operations[tag]?.cancel();
}
}
final debouncer = Debouncer();
final throttler = Throttler();

View File

@@ -1,3 +1,55 @@
import 'dart:collection';
class FixedList<T> {
final int maxLength;
final List<T> _list = [];
FixedList(this.maxLength);
add(T item) {
if (_list.length == maxLength) {
_list.removeAt(0);
}
_list.add(item);
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
}
class FixedMap<K, V> {
final int maxSize;
final Map<K, V> _map = {};
final Queue<K> _queue = Queue<K>();
FixedMap(this.maxSize);
put(K key, V value) {
if (_map.length == maxSize) {
final oldestKey = _queue.removeFirst();
_map.remove(oldestKey);
}
_map[key] = value;
_queue.add(key);
}
clear(){
_map.clear();
_queue.clear();
}
V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length;
Map<K, V> get map => Map.unmodifiable(_map);
}
extension ListExtension<T> on List<T> {
List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList();
@@ -17,8 +69,8 @@ extension ListExtension<T> on List<T> {
}
List<T> safeSublist(int start) {
if(start <= 0) return this;
if(start > length) return [];
if (start <= 0) return this;
if (start > length) return [];
return sublist(start);
}
}

View File

@@ -15,7 +15,7 @@ class SingleInstanceLock {
Future<bool> acquire() async {
try {
final lockFilePath = await appPath.getLockFilePath();
final lockFilePath = await appPath.lockFilePath;
final lockFile = File(lockFilePath);
await lockFile.create();
_accessFile = await lockFile.open(mode: FileMode.write);

View File

@@ -11,13 +11,18 @@ class Measure {
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
);
Size computeTextSize(Text text) {
Size computeTextSize(
Text text, {
double maxWidth = double.infinity,
}) {
final textPainter = TextPainter(
text: TextSpan(text: text.data, style: text.style),
maxLines: text.maxLines,
textScaler: _textScale,
textDirection: text.textDirection ?? TextDirection.ltr,
)..layout();
)..layout(
maxWidth: maxWidth,
);
return textPainter.size;
}

View File

@@ -30,16 +30,16 @@ class Navigation {
fragment: ProfilesFragment(),
),
const NavigationItem(
icon: Icon(Icons.view_timeline),
icon: Icon(Icons.view_timeline),
label: "requests",
fragment: RequestsFragment(),
fragment: RequestsFragment(),
description: "requestsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
const NavigationItem(
icon: Icon(Icons.ballot),
icon: Icon(Icons.ballot),
label: "connections",
fragment: ConnectionsFragment(),
fragment: ConnectionsFragment(),
description: "connectionsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
@@ -49,7 +49,7 @@ class Navigation {
description: "resourcesDesc",
keep: false,
fragment: Resources(),
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
modes: [NavigationItemMode.more],
),
NavigationItem(
icon: const Icon(Icons.adb),

View File

@@ -1,15 +1,11 @@
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart';
class Other {
Color? getDelayColor(int? delay) {
@@ -34,6 +30,26 @@ class Other {
);
}
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));
@@ -165,30 +181,6 @@ class Other {
: "";
}
Future<String?> parseQRCode(Uint8List? bytes) {
return Isolate.run<String?>(() {
if (bytes == null) return null;
img.Image? image = img.decodeImage(bytes);
LuminanceSource source = RGBLuminanceSource(
image!.width,
image.height,
image
.convert(numChannels: 4)
.getBytes(order: img.ChannelOrder.abgr)
.buffer
.asInt32List(),
);
final bitmap = BinaryBitmap(GlobalHistogramBinarizer(source));
final reader = QRCodeReader();
try {
final result = reader.decode(bitmap);
return result.text;
} catch (_) {
return null;
}
});
}
String? getFileNameForDisposition(String? disposition) {
if (disposition == null) return null;
final parseValue = HeaderValue.parse(disposition);

View File

@@ -48,35 +48,40 @@ class AppPath {
return join(executableDirPath, "$appHelperService$executableExtension");
}
Future<String> getDownloadDirPath() async {
Future<String> get downloadDirPath async {
final directory = await downloadDir.future;
return directory.path;
}
Future<String> getHomeDirPath() async {
Future<String> get homeDirPath async {
final directory = await dataDir.future;
return directory.path;
}
Future<String> getLockFilePath() async {
Future<String> get lockFilePath async {
final directory = await dataDir.future;
return join(directory.path, "FlClash.lock");
}
Future<String> getProfilesPath() async {
Future<String> get sharedPreferencesPath async {
final directory = await dataDir.future;
return join(directory.path, "shared_preferences.json");
}
Future<String> get profilesPath async {
final directory = await dataDir.future;
return join(directory.path, profilesDirectoryName);
}
Future<String?> getProfilePath(String? id) async {
if (id == null) return null;
final directory = await getProfilesPath();
final directory = await profilesPath;
return join(directory, "$id.yaml");
}
Future<String?> getProvidersPath(String? id) async {
if (id == null) return null;
final directory = await getProfilesPath();
final directory = await profilesPath;
return join(directory, "providers", id);
}

View File

@@ -4,13 +4,14 @@ import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:fl_clash/common/common.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class Picker {
Future<PlatformFile?> pickerFile() async {
final filePickerResult = await FilePicker.platform.pickFiles(
withData: true,
allowMultiple: false,
initialDirectory: await appPath.getDownloadDirPath(),
initialDirectory: await appPath.downloadDirPath,
);
return filePickerResult?.files.first;
}
@@ -18,7 +19,7 @@ class Picker {
Future<String?> saveFile(String fileName, Uint8List bytes) async {
final path = await FilePicker.platform.saveFile(
fileName: fileName,
initialDirectory: await appPath.getDownloadDirPath(),
initialDirectory: await appPath.downloadDirPath,
bytes: Platform.isAndroid ? bytes : null,
);
if (!Platform.isAndroid && path != null) {
@@ -30,9 +31,14 @@ class Picker {
Future<String?> pickerConfigQRCode() async {
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
final bytes = await xFile?.readAsBytes();
if (bytes == null) return null;
final result = await other.parseQRCode(bytes);
if (xFile == null) {
return null;
}
final controller = MobileScannerController();
final capture = await controller.analyzeImage(xFile.path, formats: [
BarcodeFormat.qrCode,
]);
final result = capture?.barcodes.first.rawValue;
if (result == null || !result.isUrl) {
throw appLocalizations.pleaseUploadValidQrcode;
}

View File

@@ -1,19 +1,21 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:fl_clash/models/models.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/models.dart';
import 'constant.dart';
class Preferences {
static Preferences? _instance;
Completer<SharedPreferences> sharedPreferencesCompleter = Completer();
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
Future<bool> get isInit async => await sharedPreferencesCompleter.future != null;
Preferences._internal() {
SharedPreferences.getInstance()
.then((value) => sharedPreferencesCompleter.complete(value));
SharedPreferences.getInstance().then((value) => sharedPreferencesCompleter.complete(value))
.onError((_,__)=>sharedPreferencesCompleter.complete(null));
}
factory Preferences() {
@@ -21,52 +23,44 @@ class Preferences {
return _instance!;
}
Future<ClashConfig?> getClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
final clashConfigString = preferences.getString(clashConfigKey);
final clashConfigString = preferences?.getString(clashConfigKey);
if (clashConfigString == null) return null;
final clashConfigMap = json.decode(clashConfigString);
try {
return ClashConfig.fromJson(clashConfigMap);
} catch (e) {
debugPrint(e.toString());
return null;
}
return ClashConfig.fromJson(clashConfigMap);
}
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
final preferences = await sharedPreferencesCompleter.future;
return preferences.setString(
preferences?.setString(
clashConfigKey,
json.encode(clashConfig),
);
return true;
}
Future<Config?> getConfig() async {
final preferences = await sharedPreferencesCompleter.future;
final configString = preferences.getString(configKey);
final configString = preferences?.getString(configKey);
if (configString == null) return null;
final configMap = json.decode(configString);
try {
return Config.fromJson(configMap);
} catch (e) {
debugPrint(e.toString());
return null;
}
return Config.fromJson(configMap);
}
Future<bool> saveConfig(Config config) async {
final preferences = await sharedPreferencesCompleter.future;
return preferences.setString(
return await preferences?.setString(
configKey,
json.encode(config),
);
) ?? false;
}
clearPreferences() async {
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
sharedPreferencesIns.clear();
sharedPreferencesIns?.clear();
}
}
final preferences = Preferences();
final preferences = Preferences();

View File

@@ -49,6 +49,7 @@ class Render {
_isPaused = false;
_dispatcher.onBeginFrame = _beginFrame;
_dispatcher.onDrawFrame = _drawFrame;
_dispatcher.scheduleFrame();
debugPrint("[App] resume");
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:math';
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
@@ -15,6 +16,8 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
};
}
class BaseScrollBehavior2 extends ScrollBehavior {}
class HiddenBarScrollBehavior extends BaseScrollBehavior {
@override
Widget buildScrollbar(
@@ -40,3 +43,90 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
);
}
}
class NextClampingScrollPhysics extends ClampingScrollPhysics {
@override
Simulation? createBallisticSimulation(
ScrollMetrics position, double velocity) {
final Tolerance tolerance = toleranceFor(position);
if (position.outOfRange) {
double? end;
if (position.pixels > position.maxScrollExtent) {
end = position.maxScrollExtent;
}
if (position.pixels < position.minScrollExtent) {
end = position.minScrollExtent;
}
assert(end != null);
return ScrollSpringSimulation(
spring,
end!,
end,
min(0.0, velocity),
tolerance: tolerance,
);
}
if (velocity.abs() < tolerance.velocity) {
return null;
}
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) {
return null;
}
if (velocity < 0.0 && position.pixels <= position.minScrollExtent) {
return null;
}
return ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
tolerance: tolerance,
);
}
}
class ReverseScrollController extends ScrollController {
ReverseScrollController({
super.initialScrollOffset,
super.keepScrollOffset,
super.debugLabel,
});
@override
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition? oldPosition,
) {
return ReverseScrollPosition(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
}
class ReverseScrollPosition extends ScrollPositionWithSingleContext {
ReverseScrollPosition({
required super.physics,
required super.context,
super.initialPixels = 0.0,
super.keepScrollOffset,
super.oldPosition,
super.debugLabel,
}) : _initialPixels = initialPixels ?? 0;
final double _initialPixels;
bool _isInit = false;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (!_isInit) {
correctPixels(maxScrollExtent);
_isInit = true;
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
}

20
lib/common/view.dart Normal file
View File

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

View File

@@ -338,11 +338,27 @@ class AppController {
}
}
init() async {
final isDisclaimerAccepted = await handlerDisclaimer();
if (!isDisclaimerAccepted) {
handleExit();
_handlePreference() async {
if (await preferences.isInit) {
return;
}
final res = await globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.cacheCorrupt),
);
if (res) {
final file = File(await appPath.sharedPreferencesPath);
final isExists = await file.exists();
if (isExists) {
await file.delete();
}
}
await handleExit();
}
init() async {
await _handlePreference();
await _handlerDisclaimer();
await globalState.initCore(
appState: appState,
clashConfig: clashConfig,
@@ -473,11 +489,15 @@ class AppController {
false;
}
Future<bool> handlerDisclaimer() async {
_handlerDisclaimer() async {
if (config.appSetting.disclaimerAccepted) {
return true;
return;
}
return showDisclaimer();
final isDisclaimerAccepted = await showDisclaimer();
if (!isDisclaimerAccepted) {
await handleExit();
}
return;
}
addProfileFormURL(String url) async {
@@ -673,8 +693,8 @@ class AppController {
}
Future<List<int>> backupData() async {
final homeDirPath = await appPath.getHomeDirPath();
final profilesPath = await appPath.getProfilesPath();
final homeDirPath = await appPath.homeDirPath;
final profilesPath = await appPath.profilesPath;
final configJson = config.toJson();
final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async {
@@ -705,7 +725,7 @@ class AppController {
final zipDecoder = ZipDecoder();
return zipDecoder.decodeBytes(data);
});
final homeDirPath = await appPath.getHomeDirPath();
final homeDirPath = await appPath.homeDirPath;
final configs =
archive.files.where((item) => item.name.endsWith(".json")).toList();
final profiles =

View File

@@ -20,11 +20,13 @@ class AccessFragment extends StatefulWidget {
class _AccessFragmentState extends State<AccessFragment> {
List<String> acceptList = [];
List<String> rejectList = [];
late ScrollController _controller;
@override
void initState() {
super.initState();
_updateInitList();
_controller = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
if (appState.packages.isEmpty) {
@@ -35,6 +37,12 @@ class _AccessFragmentState extends State<AccessFragment> {
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
_updateInitList() {
final accessControl = globalState.appController.config.accessControl;
acceptList = accessControl.acceptList;
@@ -52,8 +60,8 @@ class _AccessFragmentState extends State<AccessFragment> {
rejectList: rejectList,
),
).then((_) => setState(() {
_updateInitList();
}));
_updateInitList();
}));
},
icon: const Icon(Icons.search),
);
@@ -268,39 +276,44 @@ class _AccessFragmentState extends State<AccessFragment> {
? const Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: packages.length,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value:
valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config =
globalState.appController.config;
if (accessControlMode ==
AccessControlMode.acceptSelected) {
config.accessControl =
config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl =
config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
: CommonScrollBar(
controller: _controller,
child: ListView.builder(
controller: _controller,
itemCount: packages.length,
itemExtent: 72,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value:
valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config =
globalState.appController.config;
if (accessControlMode ==
AccessControlMode.acceptSelected) {
config.accessControl =
config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl =
config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
),
),
),
],

View File

@@ -705,9 +705,7 @@ class DnsListView extends StatelessWidget {
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
context.commonScaffoldState?.actions = [
IconButton(
onPressed: () async {
final res = await globalState.showMessage(

View File

@@ -206,9 +206,7 @@ class BypassDomainItem extends StatelessWidget {
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
context.commonScaffoldState?.actions = [
IconButton(
onPressed: () async {
final res = await globalState.showMessage(
@@ -378,9 +376,7 @@ class NetworkListView extends StatelessWidget {
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
context.commonScaffoldState?.actions = [
IconButton(
onPressed: () async {
final res = await globalState.showMessage(

View File

@@ -0,0 +1,155 @@
import 'dart:async';
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/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'item.dart';
class ConnectionsFragment extends StatefulWidget {
const ConnectionsFragment({super.key});
@override
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
}
class _ConnectionsFragmentState extends State<ConnectionsFragment>
with ViewMixin {
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
const ConnectionsState(),
);
final ScrollController _scrollController = ScrollController(
keepScrollOffset: false,
);
Timer? timer;
@override
List<Widget> get actions => [
IconButton(
onPressed: () async {
clashCore.closeConnections();
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
),
];
@override
get onSearch => (value) {
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(
query: value,
);
};
@override
get onKeywordsUpdate => (keywords) {
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(keywords: keywords);
};
_updateConnections() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
timer = Timer(Duration(seconds: 1), () async {
_updateConnections();
});
});
}
@override
void initState() {
super.initState();
_updateConnections();
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
initViewState();
},
);
}
_handleBlockConnection(String id) async {
clashCore.closeConnection(id);
_connectionsStateNotifier.value = _connectionsStateNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
}
@override
void dispose() {
timer?.cancel();
_connectionsStateNotifier.dispose();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'connections' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: ValueListenableBuilder<ConnectionsState>(
valueListenable: _connectionsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullConnectionsDesc,
);
}
return CommonScrollBar(
controller: _scrollController,
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
);
},
),
);
}
}

View File

@@ -0,0 +1,150 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FindProcessBuilder extends StatelessWidget {
final Widget Function(bool value) builder;
const FindProcessBuilder({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
builder: (_, value, __) {
return builder(value);
},
);
}
}
class ConnectionItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClick;
final Widget? trailing;
const ConnectionItem({
super.key,
required this.connection,
this.onClick,
this.trailing,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
return await app?.getPackageIcon(connection.metadata.process);
}
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) {
final title = Text(
connection.desc,
style: context.textTheme.bodyLarge,
);
final subTitle = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
);
if (!Platform.isAndroid) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
title: title,
subtitle: subTitle,
trailing: trailing,
);
}
return FindProcessBuilder(
builder: (bool value) {
final leading = value
? GestureDetector(
onTap: () {
if (onClick == null) return;
final process = connection.metadata.process;
if (process.isEmpty) return;
onClick!(process);
},
child: 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;
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: leading,
title: title,
subtitle: subTitle,
trailing: trailing,
);
},
);
}
}

View File

@@ -0,0 +1,239 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'item.dart';
double _preOffset = 0;
class RequestsFragment extends StatefulWidget {
const RequestsFragment({super.key});
@override
State<RequestsFragment> createState() => _RequestsFragmentState();
}
class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
final _requestsStateNotifier =
ValueNotifier<ConnectionsState>(const ConnectionsState());
List<Connection> _requests = [];
final ScrollController _scrollController = ScrollController(
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
);
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0;
@override
get onSearch => (value) {
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
query: value,
);
};
@override
get onKeywordsUpdate => (keywords) {
_requestsStateNotifier.value =
_requestsStateNotifier.value.copyWith(keywords: keywords);
};
@override
void initState() {
super.initState();
final appController = globalState.appController;
final appState = appController.appState;
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: appState.requests,
);
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
initViewState();
},
);
}
double _calcCacheHeight(Connection item) {
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
if (cacheHeight != null) {
return cacheHeight;
}
final size = globalState.measure.computeTextSize(
Text(
item.desc,
style: context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
);
final chainsText = item.chains.join("");
final length = item.chains.length;
final chainSize = globalState.measure.computeTextSize(
Text(
chainsText,
style: context.textTheme.bodyMedium,
),
maxWidth: (_currentMaxWidth - (length - 1) * 6 - length * 24),
);
final baseHeight = globalState.measure.bodyMediumHeight;
final lines = (chainSize.height / baseHeight).round();
final computerHeight =
size.height + chainSize.height + 24 + 24 * (lines - 1);
_cacheDynamicHeightMap.put(item.id, computerHeight);
return computerHeight;
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear();
}
}
@override
void dispose() {
_requestsStateNotifier.dispose();
_scrollController.dispose();
_currentMaxWidth = 0;
_cacheDynamicHeightMap.clear();
super.dispose();
}
Widget _wrapPage(Widget child) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'requests' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: child,
);
}
updateRequestsThrottler() {
throttler.call("request", () {
final isEquality = connectionListEquality.equals(
_requests,
_requestsStateNotifier.value.connections,
);
if (isEquality) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: _requests,
);
});
}, duration: commonDuration);
}
Widget _wrapRequestsUpdate(Widget child) {
return Selector<AppState, List<Connection>>(
selector: (_, appState) => appState.requests,
shouldRebuild: (prev, next) {
final isEquality = connectionListEquality.equals(prev, next);
if (!isEquality) {
_requests = next;
updateRequestsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
return FindProcessBuilder(builder: (value) {
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return _wrapPage(
_wrapRequestsUpdate(
ValueListenableBuilder<ConnectionsState>(
valueListenable: _requestsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
final items = connections
.map<Widget>(
(connection) => ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index, __) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
),
),
),
);
},
),
),
);
});
},
);
}
}

View File

@@ -1,349 +0,0 @@
import 'dart:async';
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/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((_) async {
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(
const Duration(seconds: 1),
(timer) async {
if (!context.mounted) {
return;
}
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await 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),
),
IconButton(
onPressed: () async {
clashCore.closeConnections();
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
),
];
},
);
}
_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) async {
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
}
@override
void dispose() {
timer?.cancel();
connectionsNotifier.dispose();
_scrollController.dispose();
timer = null;
super.dispose();
}
@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: 6,
spacing: 6,
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,
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
)
],
);
},
),
);
}
}
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) async {
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await 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: 6,
spacing: 6,
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,
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: _results.length,
),
)
],
);
},
);
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -23,8 +24,7 @@ class _DashboardFragmentState extends State<DashboardFragment> {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.floatingActionButton = const StartButton();
commonScaffoldState?.actions = [
ValueListenableBuilder(

View File

@@ -3,11 +3,11 @@ export 'dashboard/dashboard.dart';
export 'tools.dart';
export 'profiles/profiles.dart';
export 'logs.dart';
export 'connections.dart';
export 'access.dart';
export 'config/config.dart';
export 'application_setting.dart';
export 'about.dart';
export 'backup_and_recovery.dart';
export 'resources.dart';
export 'requests.dart';
export 'connection/requests.dart';
export 'connection/connections.dart';

View File

@@ -1,12 +1,16 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/models.dart';
import '../widgets/widgets.dart';
double _preOffset = 0;
class LogsFragment extends StatefulWidget {
const LogsFragment({super.key});
@@ -14,48 +18,66 @@ class LogsFragment extends StatefulWidget {
State<LogsFragment> createState() => _LogsFragmentState();
}
class _LogsFragmentState extends State<LogsFragment> {
final logsNotifier = ValueNotifier<LogsAndKeywords>(const LogsAndKeywords());
final scrollController = ScrollController(
keepScrollOffset: false,
class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
final _scrollController = ScrollController(
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
);
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0;
Timer? timer;
List<Log> _logs = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appFlowingState = globalState.appController.appFlowingState;
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;
if (!logListEquality.equals(
logsNotifier.value.logs,
logs,
)) {
logsNotifier.value = logsNotifier.value.copyWith(
logs: logs,
);
}
});
});
final appController = globalState.appController;
final appFlowingState = appController.appFlowingState;
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: appFlowingState.logs,
);
}
@override
List<Widget> get actions => [
IconButton(
onPressed: () {
_handleExport();
},
icon: const Icon(
Icons.file_download_outlined,
),
),
];
@override
get onSearch => (value) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
query: value,
);
};
@override
get onKeywordsUpdate => (keywords) {
_logsStateNotifier.value =
_logsStateNotifier.value.copyWith(keywords: keywords);
};
@override
void dispose() {
timer?.cancel();
logsNotifier.dispose();
scrollController.dispose();
timer = null;
_logsStateNotifier.dispose();
_scrollController.dispose();
_cacheDynamicHeightMap.clear();
super.dispose();
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear();
}
}
_handleExport() async {
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
@@ -73,293 +95,151 @@ class _LogsFragmentState extends State<LogsFragment> {
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
showSearch(
context: context,
delegate: LogsSearchDelegate(
logs: logsNotifier.value,
),
);
},
icon: const Icon(Icons.search),
),
IconButton(
onPressed: () {
_handleExport();
},
icon: const Icon(
Icons.file_download_outlined,
),
),
];
super.initViewState();
});
}
_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,
double _calcCacheHeight(String text) {
final cacheHeight = _cacheDynamicHeightMap.get(text);
if (cacheHeight != null) {
return cacheHeight;
}
final size = globalState.measure.computeTextSize(
Text(
text,
style: globalState.appController.context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
);
_cacheDynamicHeightMap.put(text, size.height);
return size.height;
}
_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,
double _getItemHeight(Log log) {
final measure = globalState.measure;
final bodySmallHeight = measure.bodySmallHeight;
final bodyMediumHeight = measure.bodyMediumHeight;
final height = _calcCacheHeight(log.payload ?? "");
return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
}
updateLogsThrottler() {
throttler.call("logs", () {
final isEquality = logListEquality.equals(
_logs,
_logsStateNotifier.value.logs,
);
if (isEquality) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: _logs,
);
});
}, duration: commonDuration);
}
Widget _wrapLogsUpdate(Widget child) {
return Selector<AppFlowingState, List<Log>>(
selector: (_, appFlowingState) => appFlowingState.logs,
shouldRebuild: (prev, next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'logs' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: ValueListenableBuilder<LogsAndKeywords>(
valueListenable: logsNotifier,
builder: (_, state, __) {
final logs = state.filteredLogs;
if (logs.isEmpty) {
return NullStatus(
label: appLocalizations.nullLogsDesc,
);
}
final reversedLogs = logs.reversed.toList();
final logWidgets = reversedLogs
.map<Widget>(
(log) => LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: _addKeyword,
),
)
.separated(
const Divider(
height: 0,
),
)
.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);
return LayoutBuilder(
builder: (_, constraints) {
_handleTryClearCache(constraints.maxWidth - 40);
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'logs' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: _wrapLogsUpdate(
Align(
alignment: Alignment.topCenter,
child: ValueListenableBuilder<LogsState>(
valueListenable: _logsStateNotifier,
builder: (_, state, __) {
final logs = state.list;
if (logs.isEmpty) {
return NullStatus(
label: appLocalizations.nullLogsDesc,
);
}
final items = logs
.map<Widget>(
(log) => LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
],
),
),
Expanded(
child: LayoutBuilder(
builder: (_, constraints) {
return ScrollConfiguration(
behavior: ShowBarScrollBehavior(),
child: ListView.builder(
controller: scrollController,
itemExtentBuilder: (index, __) {
final widget = logWidgets[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyLargeSize = measure.bodyLargeSize;
final bodySmallHeight = measure.bodySmallHeight;
final bodyMediumHeight = measure.bodyMediumHeight;
final log = reversedLogs[(index / 2).floor()];
final width = (log.payload?.length ?? 0) *
bodyLargeSize.width +
200;
final lines = (width / constraints.maxWidth).ceil();
return lines * bodyLargeSize.height +
bodySmallHeight +
8 +
bodyMediumHeight +
40;
},
itemBuilder: (_, index) {
return logWidgets[index];
},
itemCount: logWidgets.length,
));
},
),
)
],
);
},
),
);
}
}
class LogsSearchDelegate extends SearchDelegate {
ValueNotifier<LogsAndKeywords> logsNotifier;
LogsSearchDelegate({
required LogsAndKeywords logs,
}) : logsNotifier = ValueNotifier(logs);
@override
void dispose() {
logsNotifier.dispose();
super.dispose();
}
get state => logsNotifier.value;
List<Log> get _results {
final lowQuery = query.toLowerCase();
return logsNotifier.value.filteredLogs
.where(
(log) =>
(log.payload?.toLowerCase().contains(lowQuery) ?? false) ||
log.logLevel.name.contains(lowQuery),
)
.toList();
}
@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);
}
_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 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: 6,
spacing: 6,
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);
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemBuilder: (_, index) {
return items[index];
},
itemExtentBuilder: (index, __) {
final item = items[index];
if (item.runtimeType == Divider) {
return 0;
}
final log = logs[(index / 2).floor()];
return _getItemHeight(log);
},
itemCount: items.length,
),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: _results.length,
),
)
],
),
),
);
},
);
}
}
class LogItem extends StatefulWidget {
class LogItem extends StatelessWidget {
final Log log;
final Function(String)? onClick;
@@ -369,14 +249,8 @@ class LogItem extends StatefulWidget {
this.onClick,
});
@override
State<LogItem> createState() => _LogItemState();
}
class _LogItemState extends State<LogItem> {
@override
Widget build(BuildContext context) {
final log = widget.log;
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
@@ -384,14 +258,16 @@ class _LogItemState extends State<LogItem> {
),
title: SelectableText(
log.payload ?? '',
style: context.textTheme.bodyLarge,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
"${log.dateTime}",
style: context.textTheme.bodySmall
?.copyWith(color: context.colorScheme.primary),
style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.primary,
),
),
const SizedBox(
height: 8,
@@ -400,8 +276,8 @@ class _LogItemState extends State<LogItem> {
alignment: Alignment.centerLeft,
child: CommonChip(
onPressed: () {
if (widget.onClick == null) return;
widget.onClick!(log.logLevel.name);
if (onClick == null) return;
onClick!(log.logLevel.name);
},
label: log.logLevel.name,
),
@@ -411,3 +287,11 @@ class _LogItemState extends State<LogItem> {
);
}
}
class NoGlowScrollBehavior extends ScrollBehavior {
@override
Widget buildOverscrollIndicator(
BuildContext context, Widget child, ScrollableDetails details) {
return child; // 禁用过度滚动效果
}
}

View File

@@ -74,8 +74,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (!mounted) return;
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = [
IconButton(
onPressed: () {

View File

@@ -30,7 +30,7 @@ double get listHeaderHeight {
double getItemHeight(ProxyCardType proxyCardType) {
final measure = globalState.measure;
final baseHeight =
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8 + 4;
return switch (proxyCardType) {
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
ProxyCardType.shrink => baseHeight,

View File

@@ -273,13 +273,8 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
type: state.proxyCardType,
);
final itemsOffset = _getItemHeightList(items, state.proxyCardType);
return Scrollbar(
return CommonScrollBar(
controller: _controller,
thumbVisibility: true,
trackVisibility: true,
thickness: 8,
radius: const Radius.circular(8),
interactive: true,
child: Stack(
children: [
Positioned.fill(

View File

@@ -28,9 +28,7 @@ class _ProvidersState extends State<Providers> {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
globalState.appController.updateProviders();
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
context.commonScaffoldState?.actions = [
IconButton(
onPressed: () {
_updateProviders();

View File

@@ -21,10 +21,8 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey();
_initActions(ProxiesType proxiesType, bool hasProvider) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
WidgetsBinding.instance.addPostFrameCallback((_) {
context.commonScaffoldState?.actions = [
if (hasProvider) ...[
IconButton(
onPressed: () {

View File

@@ -320,9 +320,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DelayTestButton(
context.commonScaffoldState?.floatingActionButton = DelayTestButton(
onClick: () async {
await _delayTest();
},

View File

@@ -1,317 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class RequestsFragment extends StatefulWidget {
const RequestsFragment({super.key});
@override
State<RequestsFragment> createState() => _RequestsFragmentState();
}
class _RequestsFragmentState extends State<RequestsFragment> {
final requestsNotifier =
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
final ScrollController _scrollController = ScrollController(
keepScrollOffset: false,
);
Timer? timer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
requestsNotifier.value =
requestsNotifier.value.copyWith(connections: appState.requests);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final maxLength = Platform.isAndroid ? 1000 : 60;
final requests = appState.requests.safeSublist(
appState.requests.length - maxLength,
);
if (!connectionListEquality.equals(
requestsNotifier.value.connections,
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),
),
];
},
);
}
_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() {
timer?.cancel();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'requests' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: 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: 6,
spacing: 6,
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,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
)
],
);
},
),
);
}
}
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: 6,
spacing: 6,
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: (value) {
_addKeyword(value);
},
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: _results.length,
),
)
],
);
},
);
}
}

View File

@@ -112,7 +112,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
}
Future<FileInfo> _getGeoFileLastModified(String fileName) async {
final homePath = await appPath.getHomeDirPath();
final homePath = await appPath.homeDirPath;
final file = File(join(homePath, fileName));
final lastModified = await file.lastModified();
final size = await file.length();
@@ -183,7 +183,12 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
}
_handleUpdateGeoDataItem() async {
await globalState.safeRun<void>(updateGeoDateItem);
await globalState.safeRun<void>(
() async {
await updateGeoDateItem();
},
silence: false,
);
setState(() {});
}
@@ -196,6 +201,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
geoType: geoItem.label,
),
);
print(message);
if (message.isNotEmpty) throw message;
} catch (e) {
isUpdating.value = false;

View File

@@ -35,6 +35,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
delegate: OpenDelegate(
title: Intl.message(navigationItem.label),
widget: navigationItem.fragment,
extendPageWidth: 360,
),
);
}
@@ -195,14 +196,17 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
Selector<AppState, MoreToolsSelectorState>(
selector: (_, appState) {
return MoreToolsSelectorState(
navigationItems: appState.viewMode == ViewMode.mobile
? appState.navigationItems.where(
(element) {
return element.modes
.contains(NavigationItemMode.more);
},
).toList()
: [],
navigationItems: appState.navigationItems.where((element) {
final isMore =
element.modes.contains(NavigationItemMode.more);
final isDesktop =
element.modes.contains(NavigationItemMode.desktop);
if (isMore && !isDesktop) return true;
if (appState.viewMode != ViewMode.mobile || !isMore) {
return false;
}
return true;
}).toList(),
);
},
builder: (_, state, __) {

View File

@@ -341,5 +341,6 @@
"nullProxies": "No proxies",
"copySuccess": "Copy success",
"copyLink": "Copy link",
"exportFile": "Export file"
"exportFile": "Export file",
"cacheCorrupt": "The cache is corrupt. Do you want to clear it?"
}

View File

@@ -341,5 +341,6 @@
"nullProxies": "暂无代理",
"copySuccess": "复制成功",
"copyLink": "复制链接",
"exportFile": "导出文件"
"exportFile": "导出文件",
"cacheCorrupt": "缓存已损坏,是否清空?"
}

View File

@@ -39,8 +39,10 @@ MessageLookupByLibrary? _findExact(String localeName) {
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) {
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
localeName,
(locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null,
);
if (availableLocale == null) {
return new SynchronousFuture(false);
}
@@ -60,8 +62,11 @@ bool _messagesExistFor(String locale) {
}
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale =
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
var actualLocale = Intl.verifiedLocale(
locale,
_messagesExistFor,
onFailure: (_) => null,
);
if (actualLocale == null) return null;
return _findExact(actualLocale);
}

File diff suppressed because it is too large Load Diff

View File

@@ -22,390 +22,391 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"about": MessageLookupByLibrary.simpleMessage("关于"),
"accessControl": MessageLookupByLibrary.simpleMessage("访问控制"),
"accessControlAllowDesc":
MessageLookupByLibrary.simpleMessage("只允许选中应用进入VPN"),
"accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"),
"accessControlNotAllowDesc":
MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"),
"account": MessageLookupByLibrary.simpleMessage("账号"),
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
"action": MessageLookupByLibrary.simpleMessage("操作"),
"action_mode": MessageLookupByLibrary.simpleMessage("切换模式"),
"action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"action_start": MessageLookupByLibrary.simpleMessage("启动/停止"),
"action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"),
"add": MessageLookupByLibrary.simpleMessage("添加"),
"address": MessageLookupByLibrary.simpleMessage("地址"),
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
"adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理员自启动"),
"adminAutoLaunchDesc":
MessageLookupByLibrary.simpleMessage("使用管理员模式开机自启动"),
"ago": MessageLookupByLibrary.simpleMessage(""),
"agree": MessageLookupByLibrary.simpleMessage("同意"),
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
"allowBypassDesc":
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
"app": MessageLookupByLibrary.simpleMessage("应用"),
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
"appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"),
"application": MessageLookupByLibrary.simpleMessage("应用程序"),
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
"auto": MessageLookupByLibrary.simpleMessage("自动"),
"autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"),
"autoCheckUpdateDesc":
MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"),
"autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"),
"autoCloseConnectionsDesc":
MessageLookupByLibrary.simpleMessage("切换节点后自动关闭连接"),
"autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"),
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"),
"autoRun": MessageLookupByLibrary.simpleMessage("自动运行"),
"autoRunDesc": MessageLookupByLibrary.simpleMessage("应用打开时自动运行"),
"autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"),
"autoUpdateInterval":
MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"),
"backup": MessageLookupByLibrary.simpleMessage("备份"),
"backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"),
"backupAndRecoveryDesc":
MessageLookupByLibrary.simpleMessage("通过WebDAV或者文件同步数据"),
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
"bind": MessageLookupByLibrary.simpleMessage("绑定"),
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
"bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"),
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"),
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
"cancelFilterSystemApp":
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
"checkError": MessageLookupByLibrary.simpleMessage("检测失败"),
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"),
"clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"),
"columns": MessageLookupByLibrary.simpleMessage("列数"),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
"compatibleDesc":
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"),
"confirm": MessageLookupByLibrary.simpleMessage("确定"),
"connections": MessageLookupByLibrary.simpleMessage("连接"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"copy": MessageLookupByLibrary.simpleMessage("复制"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
"copyLink": MessageLookupByLibrary.simpleMessage("复制链接"),
"copySuccess": 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(""),
"defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"),
"defaultNameserverDesc":
MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"),
"defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"),
"defaultText": MessageLookupByLibrary.simpleMessage("默认"),
"delay": MessageLookupByLibrary.simpleMessage("延迟"),
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
"delete": MessageLookupByLibrary.simpleMessage("删除"),
"deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"),
"desc": MessageLookupByLibrary.simpleMessage(
"基于ClashMeta的多平台代理客户端简单易用开源无广告。"),
"direct": MessageLookupByLibrary.simpleMessage("直连"),
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
"本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。"),
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"),
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"),
"doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"),
"domain": MessageLookupByLibrary.simpleMessage("域名"),
"download": MessageLookupByLibrary.simpleMessage("下载"),
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"),
"entries": MessageLookupByLibrary.simpleMessage("个条目"),
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
"excludeDesc":
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
"exit": MessageLookupByLibrary.simpleMessage("退出"),
"expand": MessageLookupByLibrary.simpleMessage("标准"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
"exportFile": MessageLookupByLibrary.simpleMessage("导出文件"),
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc":
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
"externalLink": MessageLookupByLibrary.simpleMessage("外部链接"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"),
"fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"),
"fallback": MessageLookupByLibrary.simpleMessage("Fallback"),
"fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"),
"fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"),
"file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"),
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
"findProcessModeDesc":
MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
"fontFamily": MessageLookupByLibrary.simpleMessage("字体"),
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
"general": MessageLookupByLibrary.simpleMessage("基础"),
"generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"),
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
"geodataLoaderDesc":
MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
"geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"),
"global": MessageLookupByLibrary.simpleMessage("全局"),
"go": MessageLookupByLibrary.simpleMessage("前往"),
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hasCacheChange": MessageLookupByLibrary.simpleMessage("是否缓存修改"),
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"),
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"),
"hotkeyManagementDesc":
MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"),
"hours": MessageLookupByLibrary.simpleMessage("小时"),
"icon": MessageLookupByLibrary.simpleMessage("图片"),
"iconConfiguration": MessageLookupByLibrary.simpleMessage("片配置"),
"iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"init": MessageLookupByLibrary.simpleMessage("初始化"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"),
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"keepAliveIntervalDesc":
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
"key": MessageLookupByLibrary.simpleMessage(""),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"layout": MessageLookupByLibrary.simpleMessage("布局"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),
"list": MessageLookupByLibrary.simpleMessage("列表"),
"local": MessageLookupByLibrary.simpleMessage("本地"),
"localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"),
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"),
"logLevel": MessageLookupByLibrary.simpleMessage("日志等级"),
"logcat": MessageLookupByLibrary.simpleMessage("日志捕获"),
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"logs": MessageLookupByLibrary.simpleMessage("日志"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
"loose": MessageLookupByLibrary.simpleMessage("宽松"),
"memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"minimizeOnExitDesc":
MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
"minutes": MessageLookupByLibrary.simpleMessage("分钟"),
"mode": MessageLookupByLibrary.simpleMessage("模式"),
"months": MessageLookupByLibrary.simpleMessage(""),
"more": MessageLookupByLibrary.simpleMessage("更多"),
"name": MessageLookupByLibrary.simpleMessage(""),
"nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"),
"nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"),
"nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域名"),
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"),
"nameserverPolicyDesc":
MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"),
"network": MessageLookupByLibrary.simpleMessage("网络"),
"networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"),
"networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"),
"networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"),
"noData": MessageLookupByLibrary.simpleMessage("暂无数据"),
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
"noIcon": MessageLookupByLibrary.simpleMessage("无图标"),
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"),
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
"noProxyDesc":
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
"notEmpty": MessageLookupByLibrary.simpleMessage("不能为空"),
"notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"),
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"),
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"),
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
"nullProfileDesc":
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullProxies": MessageLookupByLibrary.simpleMessage("暂无代理"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
"onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"),
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
"onlyStatisticsProxyDesc":
MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"),
"options": MessageLookupByLibrary.simpleMessage("选项"),
"other": MessageLookupByLibrary.simpleMessage("其他"),
"otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"),
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
"override": MessageLookupByLibrary.simpleMessage("覆写"),
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
"overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"),
"overrideDnsDesc":
MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"),
"password": MessageLookupByLibrary.simpleMessage("密码"),
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
"pleaseInputAdminPassword":
MessageLookupByLibrary.simpleMessage("请输入管理员密码"),
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
"pleaseUploadValidQrcode":
MessageLookupByLibrary.simpleMessage("请上传有效的二维码"),
"port": MessageLookupByLibrary.simpleMessage("端口"),
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
"pressKeyboard": MessageLookupByLibrary.simpleMessage("按下按键"),
"preview": MessageLookupByLibrary.simpleMessage("预览"),
"profile": MessageLookupByLibrary.simpleMessage("配置"),
"profileAutoUpdateIntervalInvalidValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"),
"profileAutoUpdateIntervalNullValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"),
"profileHasUpdate":
MessageLookupByLibrary.simpleMessage("配置文件已经修改,是否关闭自动更新 "),
"profileNameNullValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入配置名称"),
"profileParseErrorDesc":
MessageLookupByLibrary.simpleMessage("配置文件解析错误"),
"profileUrlInvalidValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入有效配置URL"),
"profileUrlNullValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入配置URL"),
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
"project": MessageLookupByLibrary.simpleMessage("项目"),
"providers": MessageLookupByLibrary.simpleMessage("提供者"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理"),
"proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"),
"proxyNameserverDesc":
MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
"proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"),
"prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"),
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"regExp": MessageLookupByLibrary.simpleMessage("正则"),
"remote": MessageLookupByLibrary.simpleMessage("远程"),
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
"remoteRecoveryDesc":
MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
"remove": MessageLookupByLibrary.simpleMessage("移除"),
"requests": MessageLookupByLibrary.simpleMessage("请求"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
"reset": MessageLookupByLibrary.simpleMessage("重置"),
"resetTip": MessageLookupByLibrary.simpleMessage("确定要重置吗?"),
"resources": MessageLookupByLibrary.simpleMessage("资源"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
"DNS连接跟随rules,需配置proxy-server-nameserver"),
"routeAddress": MessageLookupByLibrary.simpleMessage("路由地址"),
"routeAddressDesc": MessageLookupByLibrary.simpleMessage("配置监听路由地址"),
"routeMode": MessageLookupByLibrary.simpleMessage("路由模式"),
"routeMode_bypassPrivate":
MessageLookupByLibrary.simpleMessage("绕过私有路由地址"),
"routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"),
"rule": MessageLookupByLibrary.simpleMessage("规则"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
"search": MessageLookupByLibrary.simpleMessage("搜索"),
"seconds": MessageLookupByLibrary.simpleMessage(""),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
"settings": MessageLookupByLibrary.simpleMessage("设置"),
"show": MessageLookupByLibrary.simpleMessage("显示"),
"shrink": MessageLookupByLibrary.simpleMessage("紧凑"),
"silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"),
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
"size": MessageLookupByLibrary.simpleMessage("尺寸"),
"sort": MessageLookupByLibrary.simpleMessage("排序"),
"source": MessageLookupByLibrary.simpleMessage("来源"),
"stackMode": MessageLookupByLibrary.simpleMessage("栈模式"),
"standard": MessageLookupByLibrary.simpleMessage("标准"),
"start": MessageLookupByLibrary.simpleMessage("启动"),
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"status": MessageLookupByLibrary.simpleMessage("状态"),
"statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"),
"stop": MessageLookupByLibrary.simpleMessage("暂停"),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"style": MessageLookupByLibrary.simpleMessage("风格"),
"submit": MessageLookupByLibrary.simpleMessage("提交"),
"sync": MessageLookupByLibrary.simpleMessage("同步"),
"system": MessageLookupByLibrary.simpleMessage("系统"),
"systemFont": MessageLookupByLibrary.simpleMessage("系统字体"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"),
"tab": MessageLookupByLibrary.simpleMessage("标签页"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
"tabAnimationDesc":
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
"theme": MessageLookupByLibrary.simpleMessage("主题"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
"tight": MessageLookupByLibrary.simpleMessage("紧凑"),
"time": MessageLookupByLibrary.simpleMessage("时间"),
"tip": MessageLookupByLibrary.simpleMessage("提示"),
"toggle": MessageLookupByLibrary.simpleMessage("切换"),
"tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"),
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
"update": MessageLookupByLibrary.simpleMessage("更新"),
"upload": MessageLookupByLibrary.simpleMessage("上传"),
"url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"),
"useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"),
"useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"),
"value": MessageLookupByLibrary.simpleMessage(""),
"view": MessageLookupByLibrary.simpleMessage("查看"),
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
"vpnEnableDesc":
MessageLookupByLibrary.simpleMessage("通过VpnService自动路由系统所有流量"),
"vpnSystemProxyDesc":
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
"vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),
"years": MessageLookupByLibrary.simpleMessage(""),
"zh_CN": MessageLookupByLibrary.simpleMessage("中文简体")
};
"about": MessageLookupByLibrary.simpleMessage("关于"),
"accessControl": MessageLookupByLibrary.simpleMessage("访问控制"),
"accessControlAllowDesc": MessageLookupByLibrary.simpleMessage(
"只允许选中应用进入VPN",
),
"accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"),
"accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage(
"选中应用将会被排除在VPN之外",
),
"account": MessageLookupByLibrary.simpleMessage("账号"),
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
"action": MessageLookupByLibrary.simpleMessage("操作"),
"action_mode": MessageLookupByLibrary.simpleMessage("切换模式"),
"action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"action_start": MessageLookupByLibrary.simpleMessage("启动/停止"),
"action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"),
"add": MessageLookupByLibrary.simpleMessage("添加"),
"address": MessageLookupByLibrary.simpleMessage("地址"),
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
"adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理员自启动"),
"adminAutoLaunchDesc": MessageLookupByLibrary.simpleMessage("使用管理员模式开机自启动"),
"ago": MessageLookupByLibrary.simpleMessage(""),
"agree": MessageLookupByLibrary.simpleMessage("同意"),
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
"allowBypassDesc": MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
"app": MessageLookupByLibrary.simpleMessage("应用"),
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
"appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"),
"application": MessageLookupByLibrary.simpleMessage("应用程序"),
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
"auto": MessageLookupByLibrary.simpleMessage("自动"),
"autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"),
"autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"),
"autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"),
"autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
"切换节点后自动关闭连接",
),
"autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"),
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"),
"autoRun": MessageLookupByLibrary.simpleMessage("自动运行"),
"autoRunDesc": MessageLookupByLibrary.simpleMessage("应用打开时自动运行"),
"autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"),
"autoUpdateInterval": MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"),
"backup": MessageLookupByLibrary.simpleMessage("备份"),
"backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"),
"backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage(
"通过WebDAV或者文件同步数据",
),
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
"bind": MessageLookupByLibrary.simpleMessage("绑定"),
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
"bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"),
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"),
"cacheCorrupt": MessageLookupByLibrary.simpleMessage("缓存已损坏,是否清空?"),
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
"cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
"checkError": MessageLookupByLibrary.simpleMessage("检测失败"),
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"),
"clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"),
"columns": MessageLookupByLibrary.simpleMessage("列数"),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
"compatibleDesc": MessageLookupByLibrary.simpleMessage(
"开启将失去部分应用能力获得全量的Clash的支持",
),
"confirm": MessageLookupByLibrary.simpleMessage("确定"),
"connections": MessageLookupByLibrary.simpleMessage("连接"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"copy": MessageLookupByLibrary.simpleMessage("复制"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
"copyLink": MessageLookupByLibrary.simpleMessage("复制链接"),
"copySuccess": 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(""),
"defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"),
"defaultNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"),
"defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"),
"defaultText": MessageLookupByLibrary.simpleMessage("默认"),
"delay": MessageLookupByLibrary.simpleMessage("延迟"),
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
"delete": MessageLookupByLibrary.simpleMessage("删除"),
"deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"),
"desc": MessageLookupByLibrary.simpleMessage(
"基于ClashMeta的多平台代理客户端简单易用开源无广告。",
),
"direct": MessageLookupByLibrary.simpleMessage("直连"),
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
"本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。",
),
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"),
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"),
"doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"),
"domain": MessageLookupByLibrary.simpleMessage("域名"),
"download": MessageLookupByLibrary.simpleMessage("下载"),
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"),
"entries": MessageLookupByLibrary.simpleMessage("个条目"),
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
"excludeDesc": MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
"exit": MessageLookupByLibrary.simpleMessage("退出"),
"expand": MessageLookupByLibrary.simpleMessage("标准"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
"exportFile": MessageLookupByLibrary.simpleMessage("导出文件"),
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"开启后将可以通过9090端口控制Clash内核",
),
"externalLink": MessageLookupByLibrary.simpleMessage("外部链接"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"),
"fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"),
"fallback": MessageLookupByLibrary.simpleMessage("Fallback"),
"fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"),
"fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"),
"file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"),
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
"findProcessModeDesc": MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
"fontFamily": MessageLookupByLibrary.simpleMessage("字体"),
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
"general": MessageLookupByLibrary.simpleMessage("基础"),
"generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"),
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
"geodataLoaderDesc": MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
"geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"),
"global": MessageLookupByLibrary.simpleMessage("全局"),
"go": MessageLookupByLibrary.simpleMessage("前往"),
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hasCacheChange": MessageLookupByLibrary.simpleMessage("是否缓存修改"),
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"),
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"),
"hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"),
"hours": MessageLookupByLibrary.simpleMessage("小时"),
"icon": MessageLookupByLibrary.simpleMessage("图片"),
"iconConfiguration": MessageLookupByLibrary.simpleMessage("图片配置"),
"iconStyle": MessageLookupByLibrary.simpleMessage("标样式"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"init": MessageLookupByLibrary.simpleMessage("初始化"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"),
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
"key": MessageLookupByLibrary.simpleMessage(""),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"layout": MessageLookupByLibrary.simpleMessage("布局"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),
"list": MessageLookupByLibrary.simpleMessage("列表"),
"local": MessageLookupByLibrary.simpleMessage("本地"),
"localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"),
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"),
"logLevel": MessageLookupByLibrary.simpleMessage("日志等级"),
"logcat": MessageLookupByLibrary.simpleMessage("日志捕获"),
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"logs": MessageLookupByLibrary.simpleMessage("日志"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
"loose": MessageLookupByLibrary.simpleMessage("宽松"),
"memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
"minutes": MessageLookupByLibrary.simpleMessage("分钟"),
"mode": MessageLookupByLibrary.simpleMessage("模式"),
"months": MessageLookupByLibrary.simpleMessage(""),
"more": MessageLookupByLibrary.simpleMessage("更多"),
"name": MessageLookupByLibrary.simpleMessage("名称"),
"nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"),
"nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"),
"nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域"),
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"),
"nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"),
"network": MessageLookupByLibrary.simpleMessage("网络"),
"networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"),
"networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"),
"networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"),
"noData": MessageLookupByLibrary.simpleMessage("暂无数据"),
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
"noIcon": MessageLookupByLibrary.simpleMessage("无图标"),
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"),
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
"noProxyDesc": MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
"notEmpty": MessageLookupByLibrary.simpleMessage("不能为空"),
"notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"),
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"),
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"),
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
"nullProfileDesc": MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullProxies": MessageLookupByLibrary.simpleMessage("暂无代理"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
"onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"),
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
"onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage(
"开启后,将只统计代理流量",
),
"options": MessageLookupByLibrary.simpleMessage("选项"),
"other": MessageLookupByLibrary.simpleMessage("其他"),
"otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"),
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
"override": MessageLookupByLibrary.simpleMessage("覆写"),
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
"overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"),
"overrideDnsDesc": MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"),
"password": MessageLookupByLibrary.simpleMessage("密码"),
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
"pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage(
"请输入管理员密码",
),
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
"pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(
"请上传有效的二维码",
),
"port": MessageLookupByLibrary.simpleMessage("端口"),
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
"pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"),
"preview": MessageLookupByLibrary.simpleMessage("预览"),
"profile": MessageLookupByLibrary.simpleMessage("配置"),
"profileAutoUpdateIntervalInvalidValidationDesc":
MessageLookupByLibrary.simpleMessage("输入有效间隔时间格式"),
"profileAutoUpdateIntervalNullValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"),
"profileHasUpdate": MessageLookupByLibrary.simpleMessage(
"配置文件已经修改,是否关闭自动更新 ",
),
"profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"请输入配置名称",
),
"profileParseErrorDesc": MessageLookupByLibrary.simpleMessage("配置文件解析错误"),
"profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage(
"请输入有效配置URL",
),
"profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"请输入配置URL",
),
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
"project": MessageLookupByLibrary.simpleMessage("项目"),
"providers": MessageLookupByLibrary.simpleMessage("提供者"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理"),
"proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"),
"proxyNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
"proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"),
"prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"),
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("恢复配置文件"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"regExp": MessageLookupByLibrary.simpleMessage("正则"),
"remote": MessageLookupByLibrary.simpleMessage("远程"),
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
"remoteRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
"remove": MessageLookupByLibrary.simpleMessage("移除"),
"requests": MessageLookupByLibrary.simpleMessage("请求"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
"reset": MessageLookupByLibrary.simpleMessage("重置"),
"resetTip": MessageLookupByLibrary.simpleMessage("确定要重置吗?"),
"resources": MessageLookupByLibrary.simpleMessage("资源"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
"DNS连接跟随rules,需配置proxy-server-nameserver",
),
"routeAddress": MessageLookupByLibrary.simpleMessage("路由地址"),
"routeAddressDesc": MessageLookupByLibrary.simpleMessage("配置监听路由地址"),
"routeMode": MessageLookupByLibrary.simpleMessage("路由模式"),
"routeMode_bypassPrivate": MessageLookupByLibrary.simpleMessage("绕过私有路由地址"),
"routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"),
"rule": MessageLookupByLibrary.simpleMessage("规则"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
"search": MessageLookupByLibrary.simpleMessage("搜索"),
"seconds": MessageLookupByLibrary.simpleMessage(""),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
"settings": MessageLookupByLibrary.simpleMessage("设置"),
"show": MessageLookupByLibrary.simpleMessage("显示"),
"shrink": MessageLookupByLibrary.simpleMessage("紧凑"),
"silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"),
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
"size": MessageLookupByLibrary.simpleMessage("尺寸"),
"sort": MessageLookupByLibrary.simpleMessage("排序"),
"source": MessageLookupByLibrary.simpleMessage("来源"),
"stackMode": MessageLookupByLibrary.simpleMessage("栈模式"),
"standard": MessageLookupByLibrary.simpleMessage("标准"),
"start": MessageLookupByLibrary.simpleMessage("启动"),
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"status": MessageLookupByLibrary.simpleMessage("状态"),
"statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"),
"stop": MessageLookupByLibrary.simpleMessage("暂停"),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"style": MessageLookupByLibrary.simpleMessage("风格"),
"submit": MessageLookupByLibrary.simpleMessage("提交"),
"sync": MessageLookupByLibrary.simpleMessage("同步"),
"system": MessageLookupByLibrary.simpleMessage("系统"),
"systemFont": MessageLookupByLibrary.simpleMessage("系统字体"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"),
"tab": MessageLookupByLibrary.simpleMessage("标签页"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
"tabAnimationDesc": MessageLookupByLibrary.simpleMessage(
"开启后,主页选项卡将添加切换动画",
),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
"theme": MessageLookupByLibrary.simpleMessage("主题"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
"tight": MessageLookupByLibrary.simpleMessage("紧凑"),
"time": MessageLookupByLibrary.simpleMessage("时间"),
"tip": MessageLookupByLibrary.simpleMessage("提示"),
"toggle": MessageLookupByLibrary.simpleMessage("切换"),
"tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
"unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage(
"无法更新当前配置文件",
),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"),
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
"update": MessageLookupByLibrary.simpleMessage("更新"),
"upload": MessageLookupByLibrary.simpleMessage("上传"),
"url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"),
"useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"),
"useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"),
"value": MessageLookupByLibrary.simpleMessage(""),
"view": MessageLookupByLibrary.simpleMessage("查看"),
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
"vpnEnableDesc": MessageLookupByLibrary.simpleMessage(
"通过VpnService自动路由系统所有流量",
),
"vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage(
"为VpnService附加HTTP代理",
),
"vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),
"years": MessageLookupByLibrary.simpleMessage(""),
"zh_CN": MessageLookupByLibrary.simpleMessage("中文简体"),
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -27,11 +27,11 @@ Future<void> main() async {
globalState.packageInfo = await PackageInfo.fromPlatform();
final version = await system.version;
final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await AppLocalizations.load(
other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await android?.init();
await window?.init(config.windowProps, version);
final appState = AppState(
@@ -89,7 +89,7 @@ Future<void> _service(List<String> flags) async {
await ClashCore.initGeo();
globalState.packageInfo = await PackageInfo.fromPlatform();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
final homeDirPath = await appPath.getHomeDirPath();
final homeDirPath = await appPath.homeDirPath;
await app?.tip(appLocalizations.startVpn);
clashLibHandler
.quickStart(

View File

@@ -21,10 +21,11 @@ class _AppStateManagerState extends State<AppStateManager>
_updateNavigationsContainer(Widget child) {
return Selector2<AppState, Config, UpdateNavigationsSelector>(
selector: (_, appState, config) {
final group = appState.currentGroups;
final hasProfile = config.profiles.isNotEmpty;
return UpdateNavigationsSelector(
openLogs: config.appSetting.openLogs,
hasProxies: hasProfile && config.currentProfileId != null,
hasProxies: group.isNotEmpty && hasProfile,
);
},
builder: (context, state, child) {
@@ -91,7 +92,7 @@ class _AppStateManagerState extends State<AppStateManager>
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: (_) {
onPointerHover: (_) {
render?.resume();
},
child: _cacheStateChange(

View File

@@ -113,7 +113,6 @@ class _WindowContainerState extends State<WindowManager>
@override
Future<void> onTaskbarCreated() async {
globalState.appController.updateTray(true);
await globalState.appController.restartCore();
super.onTaskbarCreated();
}

View File

@@ -20,7 +20,7 @@ class AppState with ChangeNotifier {
SelectedMap _selectedMap;
List<Group> _groups;
double _viewWidth;
List<Connection> _requests;
final FixedList<Connection> _requests;
num _checkIpNum;
List<ExternalProvider> _providers;
List<Package> _packages;
@@ -31,14 +31,17 @@ class AppState with ChangeNotifier {
required Mode mode,
required SelectedMap selectedMap,
required int version,
}) : _navigationItems = [],
})
: _navigationItems = [],
_isInit = false,
_currentLabel = "dashboard",
_viewWidth = other.getScreenSize().width,
_viewWidth = other
.getScreenSize()
.width,
_selectedMap = selectedMap,
_sortNum = 0,
_checkIpNum = 0,
_requests = [],
_requests = FixedList(1000),
_mode = mode,
_brightness = null,
_delayMap = {},
@@ -76,7 +79,7 @@ class AppState with ChangeNotifier {
return navigationItems
.where(
(element) => element.modes.contains(navigationItemMode),
)
)
.toList();
}
@@ -106,7 +109,7 @@ class AppState with ChangeNotifier {
if (index == -1) return proxyName;
final group = groups[index];
final currentSelectedName =
group.getCurrentSelectedName(selectedMap[proxyName] ?? '');
group.getCurrentSelectedName(selectedMap[proxyName] ?? '');
if (currentSelectedName.isEmpty) return proxyName;
return getRealProxyName(
currentSelectedName,
@@ -131,19 +134,10 @@ class AppState with ChangeNotifier {
}
}
List<Connection> get requests => _requests;
set requests(List<Connection> value) {
if (_requests != value) {
_requests = value;
notifyListeners();
}
}
List<Connection> get requests => _requests.list;
addRequest(Connection value) {
_requests = List.from(_requests)..add(value);
const maxLength = 1000;
_requests = _requests.safeSublist(_requests.length - maxLength);
_requests.add(value);
notifyListeners();
}
@@ -273,13 +267,14 @@ class AppState with ChangeNotifier {
if (provider == null) return;
final index = _providers.indexWhere((item) => item.name == provider.name);
if (index == -1) return;
_providers = List.from(_providers)..[index] = provider;
_providers = List.from(_providers)
..[index] = provider;
notifyListeners();
}
Group? getGroupWithName(String groupName) {
final index =
currentGroups.indexWhere((element) => element.name == groupName);
currentGroups.indexWhere((element) => element.name == groupName);
return index != -1 ? currentGroups[index] : null;
}
@@ -304,13 +299,13 @@ class AppState with ChangeNotifier {
class AppFlowingState with ChangeNotifier {
int? _runTime;
List<Log> _logs;
final FixedList<Log> _logs;
List<Traffic> _traffics;
Traffic _totalTraffic;
String? _localIp;
AppFlowingState()
: _logs = [],
: _logs = FixedList(1000),
_traffics = [],
_totalTraffic = Traffic();
@@ -325,19 +320,10 @@ class AppFlowingState with ChangeNotifier {
}
}
List<Log> get logs => _logs;
set logs(List<Log> value) {
if (_logs != value) {
_logs = value;
notifyListeners();
}
}
List<Log> get logs => _logs.list;
addLog(Log log) {
_logs = List.from(_logs)..add(log);
const maxLength = 1000;
_logs = _logs.safeSublist(_logs.length - maxLength);
_logs.add(log);
notifyListeners();
}
@@ -351,7 +337,8 @@ class AppFlowingState with ChangeNotifier {
}
addTraffic(Traffic traffic) {
_traffics = List.from(_traffics)..add(traffic);
_traffics = List.from(_traffics)
..add(traffic);
const maxLength = 30;
_traffics = _traffics.safeSublist(_traffics.length - maxLength);
notifyListeners();

View File

@@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/common.freezed.dart';
part 'generated/common.g.dart';
@freezed
@@ -31,7 +30,7 @@ class Package with _$Package {
required String packageName,
required String label,
required bool isSystem,
required int firstInstallTime,
required int lastUpdateTime,
}) = _Package;
factory Package.fromJson(Map<String, Object?> json) =>
@@ -71,6 +70,19 @@ class Connection with _$Connection {
_$ConnectionFromJson(json);
}
extension ConnectionExt on Connection {
String get desc {
var text = "${metadata.network}://";
final ips = [
metadata.host,
metadata.destinationIP,
].where((ip) => ip.isNotEmpty);
text += ips.join("/");
text += ":${metadata.destinationPort}";
return text;
}
}
@JsonSerializable()
class Log {
@JsonKey(name: "LogLevel")
@@ -101,42 +113,58 @@ class Log {
}
@freezed
class LogsAndKeywords with _$LogsAndKeywords {
const factory LogsAndKeywords({
class LogsState with _$LogsState {
const factory LogsState({
@Default([]) List<Log> logs,
@Default([]) List<String> keywords,
}) = _LogsAndKeywords;
factory LogsAndKeywords.fromJson(Map<String, Object?> json) =>
_$LogsAndKeywordsFromJson(json);
@Default("") String query,
}) = _LogsState;
}
extension LogsAndKeywordsExt on LogsAndKeywords {
List<Log> get filteredLogs => logs
.where(
(log) => {log.logLevel.name}.containsAll(keywords),
)
.toList();
extension LogsStateExt on LogsState {
List<Log> get list {
final lowQuery = query.toLowerCase();
return logs.where(
(log) {
final payload = log.payload?.toLowerCase();
final logLevelName = log.logLevel.name;
return {logLevelName}.containsAll(keywords) &&
((payload?.contains(lowQuery) ?? false) ||
logLevelName.contains(lowQuery));
},
).toList();
}
}
@freezed
class ConnectionsAndKeywords with _$ConnectionsAndKeywords {
const factory ConnectionsAndKeywords({
class ConnectionsState with _$ConnectionsState {
const factory ConnectionsState({
@Default([]) List<Connection> connections,
@Default([]) List<String> keywords,
}) = _ConnectionsAndKeywords;
factory ConnectionsAndKeywords.fromJson(Map<String, Object?> json) =>
_$ConnectionsAndKeywordsFromJson(json);
@Default("") String query,
}) = _ConnectionsState;
}
extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords {
List<Connection> get filteredConnections => connections
.where((connection) => {
...connection.chains,
connection.metadata.process,
}.containsAll(keywords))
.toList();
extension ConnectionsStateExt on ConnectionsState {
List<Connection> get list {
final lowerQuery = query.toLowerCase().trim();
final lowQuery = query.toLowerCase();
return connections.where((connection) {
final chains = connection.chains;
final process = connection.metadata.process;
final networkText = connection.metadata.network.toLowerCase();
final hostText = connection.metadata.host.toLowerCase();
final destinationIPText = connection.metadata.destinationIP.toLowerCase();
final processText = connection.metadata.process.toLowerCase();
final chainsText = chains.join("").toLowerCase();
return {...chains, process}.containsAll(keywords) &&
(networkText.contains(lowerQuery) ||
hostText.contains(lowerQuery) ||
destinationIPText.contains(lowQuery) ||
processText.contains(lowerQuery) ||
chainsText.contains(lowerQuery));
}).toList();
}
}
const defaultDavFileName = "backup.zip";

View File

@@ -290,7 +290,7 @@ mixin _$Package {
String get packageName => throw _privateConstructorUsedError;
String get label => throw _privateConstructorUsedError;
bool get isSystem => throw _privateConstructorUsedError;
int get firstInstallTime => throw _privateConstructorUsedError;
int get lastUpdateTime => throw _privateConstructorUsedError;
/// Serializes this Package to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -307,7 +307,7 @@ abstract class $PackageCopyWith<$Res> {
_$PackageCopyWithImpl<$Res, Package>;
@useResult
$Res call(
{String packageName, String label, bool isSystem, int firstInstallTime});
{String packageName, String label, bool isSystem, int lastUpdateTime});
}
/// @nodoc
@@ -328,7 +328,7 @@ class _$PackageCopyWithImpl<$Res, $Val extends Package>
Object? packageName = null,
Object? label = null,
Object? isSystem = null,
Object? firstInstallTime = null,
Object? lastUpdateTime = null,
}) {
return _then(_value.copyWith(
packageName: null == packageName
@@ -343,9 +343,9 @@ class _$PackageCopyWithImpl<$Res, $Val extends Package>
? _value.isSystem
: isSystem // ignore: cast_nullable_to_non_nullable
as bool,
firstInstallTime: null == firstInstallTime
? _value.firstInstallTime
: firstInstallTime // ignore: cast_nullable_to_non_nullable
lastUpdateTime: null == lastUpdateTime
? _value.lastUpdateTime
: lastUpdateTime // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
@@ -359,7 +359,7 @@ abstract class _$$PackageImplCopyWith<$Res> implements $PackageCopyWith<$Res> {
@override
@useResult
$Res call(
{String packageName, String label, bool isSystem, int firstInstallTime});
{String packageName, String label, bool isSystem, int lastUpdateTime});
}
/// @nodoc
@@ -378,7 +378,7 @@ class __$$PackageImplCopyWithImpl<$Res>
Object? packageName = null,
Object? label = null,
Object? isSystem = null,
Object? firstInstallTime = null,
Object? lastUpdateTime = null,
}) {
return _then(_$PackageImpl(
packageName: null == packageName
@@ -393,9 +393,9 @@ class __$$PackageImplCopyWithImpl<$Res>
? _value.isSystem
: isSystem // ignore: cast_nullable_to_non_nullable
as bool,
firstInstallTime: null == firstInstallTime
? _value.firstInstallTime
: firstInstallTime // ignore: cast_nullable_to_non_nullable
lastUpdateTime: null == lastUpdateTime
? _value.lastUpdateTime
: lastUpdateTime // ignore: cast_nullable_to_non_nullable
as int,
));
}
@@ -408,7 +408,7 @@ class _$PackageImpl implements _Package {
{required this.packageName,
required this.label,
required this.isSystem,
required this.firstInstallTime});
required this.lastUpdateTime});
factory _$PackageImpl.fromJson(Map<String, dynamic> json) =>
_$$PackageImplFromJson(json);
@@ -420,11 +420,11 @@ class _$PackageImpl implements _Package {
@override
final bool isSystem;
@override
final int firstInstallTime;
final int lastUpdateTime;
@override
String toString() {
return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem, firstInstallTime: $firstInstallTime)';
return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem, lastUpdateTime: $lastUpdateTime)';
}
@override
@@ -437,14 +437,14 @@ class _$PackageImpl implements _Package {
(identical(other.label, label) || other.label == label) &&
(identical(other.isSystem, isSystem) ||
other.isSystem == isSystem) &&
(identical(other.firstInstallTime, firstInstallTime) ||
other.firstInstallTime == firstInstallTime));
(identical(other.lastUpdateTime, lastUpdateTime) ||
other.lastUpdateTime == lastUpdateTime));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, packageName, label, isSystem, firstInstallTime);
Object.hash(runtimeType, packageName, label, isSystem, lastUpdateTime);
/// Create a copy of Package
/// with the given fields replaced by the non-null parameter values.
@@ -467,7 +467,7 @@ abstract class _Package implements Package {
{required final String packageName,
required final String label,
required final bool isSystem,
required final int firstInstallTime}) = _$PackageImpl;
required final int lastUpdateTime}) = _$PackageImpl;
factory _Package.fromJson(Map<String, dynamic> json) = _$PackageImpl.fromJson;
@@ -478,7 +478,7 @@ abstract class _Package implements Package {
@override
bool get isSystem;
@override
int get firstInstallTime;
int get lastUpdateTime;
/// Create a copy of Package
/// with the given fields replaced by the non-null parameter values.
@@ -1092,51 +1092,45 @@ abstract class _Connection implements Connection {
throw _privateConstructorUsedError;
}
LogsAndKeywords _$LogsAndKeywordsFromJson(Map<String, dynamic> json) {
return _LogsAndKeywords.fromJson(json);
}
/// @nodoc
mixin _$LogsAndKeywords {
mixin _$LogsState {
List<Log> get logs => throw _privateConstructorUsedError;
List<String> get keywords => throw _privateConstructorUsedError;
String get query => throw _privateConstructorUsedError;
/// Serializes this LogsAndKeywords to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of LogsAndKeywords
/// Create a copy of LogsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LogsAndKeywordsCopyWith<LogsAndKeywords> get copyWith =>
$LogsStateCopyWith<LogsState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LogsAndKeywordsCopyWith<$Res> {
factory $LogsAndKeywordsCopyWith(
LogsAndKeywords value, $Res Function(LogsAndKeywords) then) =
_$LogsAndKeywordsCopyWithImpl<$Res, LogsAndKeywords>;
abstract class $LogsStateCopyWith<$Res> {
factory $LogsStateCopyWith(LogsState value, $Res Function(LogsState) then) =
_$LogsStateCopyWithImpl<$Res, LogsState>;
@useResult
$Res call({List<Log> logs, List<String> keywords});
$Res call({List<Log> logs, List<String> keywords, String query});
}
/// @nodoc
class _$LogsAndKeywordsCopyWithImpl<$Res, $Val extends LogsAndKeywords>
implements $LogsAndKeywordsCopyWith<$Res> {
_$LogsAndKeywordsCopyWithImpl(this._value, this._then);
class _$LogsStateCopyWithImpl<$Res, $Val extends LogsState>
implements $LogsStateCopyWith<$Res> {
_$LogsStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LogsAndKeywords
/// Create a copy of LogsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? logs = null,
Object? keywords = null,
Object? query = null,
}) {
return _then(_value.copyWith(
logs: null == logs
@@ -1147,38 +1141,43 @@ class _$LogsAndKeywordsCopyWithImpl<$Res, $Val extends LogsAndKeywords>
? _value.keywords
: keywords // ignore: cast_nullable_to_non_nullable
as List<String>,
query: null == query
? _value.query
: query // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$LogsAndKeywordsImplCopyWith<$Res>
implements $LogsAndKeywordsCopyWith<$Res> {
factory _$$LogsAndKeywordsImplCopyWith(_$LogsAndKeywordsImpl value,
$Res Function(_$LogsAndKeywordsImpl) then) =
__$$LogsAndKeywordsImplCopyWithImpl<$Res>;
abstract class _$$LogsStateImplCopyWith<$Res>
implements $LogsStateCopyWith<$Res> {
factory _$$LogsStateImplCopyWith(
_$LogsStateImpl value, $Res Function(_$LogsStateImpl) then) =
__$$LogsStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<Log> logs, List<String> keywords});
$Res call({List<Log> logs, List<String> keywords, String query});
}
/// @nodoc
class __$$LogsAndKeywordsImplCopyWithImpl<$Res>
extends _$LogsAndKeywordsCopyWithImpl<$Res, _$LogsAndKeywordsImpl>
implements _$$LogsAndKeywordsImplCopyWith<$Res> {
__$$LogsAndKeywordsImplCopyWithImpl(
_$LogsAndKeywordsImpl _value, $Res Function(_$LogsAndKeywordsImpl) _then)
class __$$LogsStateImplCopyWithImpl<$Res>
extends _$LogsStateCopyWithImpl<$Res, _$LogsStateImpl>
implements _$$LogsStateImplCopyWith<$Res> {
__$$LogsStateImplCopyWithImpl(
_$LogsStateImpl _value, $Res Function(_$LogsStateImpl) _then)
: super(_value, _then);
/// Create a copy of LogsAndKeywords
/// Create a copy of LogsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? logs = null,
Object? keywords = null,
Object? query = null,
}) {
return _then(_$LogsAndKeywordsImpl(
return _then(_$LogsStateImpl(
logs: null == logs
? _value._logs
: logs // ignore: cast_nullable_to_non_nullable
@@ -1187,21 +1186,24 @@ class __$$LogsAndKeywordsImplCopyWithImpl<$Res>
? _value._keywords
: keywords // ignore: cast_nullable_to_non_nullable
as List<String>,
query: null == query
? _value.query
: query // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$LogsAndKeywordsImpl implements _LogsAndKeywords {
const _$LogsAndKeywordsImpl(
{final List<Log> logs = const [], final List<String> keywords = const []})
class _$LogsStateImpl implements _LogsState {
const _$LogsStateImpl(
{final List<Log> logs = const [],
final List<String> keywords = const [],
this.query = ""})
: _logs = logs,
_keywords = keywords;
factory _$LogsAndKeywordsImpl.fromJson(Map<String, dynamic> json) =>
_$$LogsAndKeywordsImplFromJson(json);
final List<Log> _logs;
@override
@JsonKey()
@@ -1220,112 +1222,103 @@ class _$LogsAndKeywordsImpl implements _LogsAndKeywords {
return EqualUnmodifiableListView(_keywords);
}
@override
@JsonKey()
final String query;
@override
String toString() {
return 'LogsAndKeywords(logs: $logs, keywords: $keywords)';
return 'LogsState(logs: $logs, keywords: $keywords, query: $query)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LogsAndKeywordsImpl &&
other is _$LogsStateImpl &&
const DeepCollectionEquality().equals(other._logs, _logs) &&
const DeepCollectionEquality().equals(other._keywords, _keywords));
const DeepCollectionEquality().equals(other._keywords, _keywords) &&
(identical(other.query, query) || other.query == query));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_logs),
const DeepCollectionEquality().hash(_keywords));
const DeepCollectionEquality().hash(_keywords),
query);
/// Create a copy of LogsAndKeywords
/// Create a copy of LogsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LogsAndKeywordsImplCopyWith<_$LogsAndKeywordsImpl> get copyWith =>
__$$LogsAndKeywordsImplCopyWithImpl<_$LogsAndKeywordsImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$LogsAndKeywordsImplToJson(
this,
);
}
_$$LogsStateImplCopyWith<_$LogsStateImpl> get copyWith =>
__$$LogsStateImplCopyWithImpl<_$LogsStateImpl>(this, _$identity);
}
abstract class _LogsAndKeywords implements LogsAndKeywords {
const factory _LogsAndKeywords(
abstract class _LogsState implements LogsState {
const factory _LogsState(
{final List<Log> logs,
final List<String> keywords}) = _$LogsAndKeywordsImpl;
factory _LogsAndKeywords.fromJson(Map<String, dynamic> json) =
_$LogsAndKeywordsImpl.fromJson;
final List<String> keywords,
final String query}) = _$LogsStateImpl;
@override
List<Log> get logs;
@override
List<String> get keywords;
@override
String get query;
/// Create a copy of LogsAndKeywords
/// Create a copy of LogsState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LogsAndKeywordsImplCopyWith<_$LogsAndKeywordsImpl> get copyWith =>
_$$LogsStateImplCopyWith<_$LogsStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ConnectionsAndKeywords _$ConnectionsAndKeywordsFromJson(
Map<String, dynamic> json) {
return _ConnectionsAndKeywords.fromJson(json);
}
/// @nodoc
mixin _$ConnectionsAndKeywords {
mixin _$ConnectionsState {
List<Connection> get connections => throw _privateConstructorUsedError;
List<String> get keywords => throw _privateConstructorUsedError;
String get query => throw _privateConstructorUsedError;
/// Serializes this ConnectionsAndKeywords to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of ConnectionsAndKeywords
/// Create a copy of ConnectionsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ConnectionsAndKeywordsCopyWith<ConnectionsAndKeywords> get copyWith =>
$ConnectionsStateCopyWith<ConnectionsState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ConnectionsAndKeywordsCopyWith<$Res> {
factory $ConnectionsAndKeywordsCopyWith(ConnectionsAndKeywords value,
$Res Function(ConnectionsAndKeywords) then) =
_$ConnectionsAndKeywordsCopyWithImpl<$Res, ConnectionsAndKeywords>;
abstract class $ConnectionsStateCopyWith<$Res> {
factory $ConnectionsStateCopyWith(
ConnectionsState value, $Res Function(ConnectionsState) then) =
_$ConnectionsStateCopyWithImpl<$Res, ConnectionsState>;
@useResult
$Res call({List<Connection> connections, List<String> keywords});
$Res call(
{List<Connection> connections, List<String> keywords, String query});
}
/// @nodoc
class _$ConnectionsAndKeywordsCopyWithImpl<$Res,
$Val extends ConnectionsAndKeywords>
implements $ConnectionsAndKeywordsCopyWith<$Res> {
_$ConnectionsAndKeywordsCopyWithImpl(this._value, this._then);
class _$ConnectionsStateCopyWithImpl<$Res, $Val extends ConnectionsState>
implements $ConnectionsStateCopyWith<$Res> {
_$ConnectionsStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ConnectionsAndKeywords
/// Create a copy of ConnectionsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? connections = null,
Object? keywords = null,
Object? query = null,
}) {
return _then(_value.copyWith(
connections: null == connections
@@ -1336,41 +1329,44 @@ class _$ConnectionsAndKeywordsCopyWithImpl<$Res,
? _value.keywords
: keywords // ignore: cast_nullable_to_non_nullable
as List<String>,
query: null == query
? _value.query
: query // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$ConnectionsAndKeywordsImplCopyWith<$Res>
implements $ConnectionsAndKeywordsCopyWith<$Res> {
factory _$$ConnectionsAndKeywordsImplCopyWith(
_$ConnectionsAndKeywordsImpl value,
$Res Function(_$ConnectionsAndKeywordsImpl) then) =
__$$ConnectionsAndKeywordsImplCopyWithImpl<$Res>;
abstract class _$$ConnectionsStateImplCopyWith<$Res>
implements $ConnectionsStateCopyWith<$Res> {
factory _$$ConnectionsStateImplCopyWith(_$ConnectionsStateImpl value,
$Res Function(_$ConnectionsStateImpl) then) =
__$$ConnectionsStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<Connection> connections, List<String> keywords});
$Res call(
{List<Connection> connections, List<String> keywords, String query});
}
/// @nodoc
class __$$ConnectionsAndKeywordsImplCopyWithImpl<$Res>
extends _$ConnectionsAndKeywordsCopyWithImpl<$Res,
_$ConnectionsAndKeywordsImpl>
implements _$$ConnectionsAndKeywordsImplCopyWith<$Res> {
__$$ConnectionsAndKeywordsImplCopyWithImpl(
_$ConnectionsAndKeywordsImpl _value,
$Res Function(_$ConnectionsAndKeywordsImpl) _then)
class __$$ConnectionsStateImplCopyWithImpl<$Res>
extends _$ConnectionsStateCopyWithImpl<$Res, _$ConnectionsStateImpl>
implements _$$ConnectionsStateImplCopyWith<$Res> {
__$$ConnectionsStateImplCopyWithImpl(_$ConnectionsStateImpl _value,
$Res Function(_$ConnectionsStateImpl) _then)
: super(_value, _then);
/// Create a copy of ConnectionsAndKeywords
/// Create a copy of ConnectionsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? connections = null,
Object? keywords = null,
Object? query = null,
}) {
return _then(_$ConnectionsAndKeywordsImpl(
return _then(_$ConnectionsStateImpl(
connections: null == connections
? _value._connections
: connections // ignore: cast_nullable_to_non_nullable
@@ -1379,22 +1375,24 @@ class __$$ConnectionsAndKeywordsImplCopyWithImpl<$Res>
? _value._keywords
: keywords // ignore: cast_nullable_to_non_nullable
as List<String>,
query: null == query
? _value.query
: query // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ConnectionsAndKeywordsImpl implements _ConnectionsAndKeywords {
const _$ConnectionsAndKeywordsImpl(
class _$ConnectionsStateImpl implements _ConnectionsState {
const _$ConnectionsStateImpl(
{final List<Connection> connections = const [],
final List<String> keywords = const []})
final List<String> keywords = const [],
this.query = ""})
: _connections = connections,
_keywords = keywords;
factory _$ConnectionsAndKeywordsImpl.fromJson(Map<String, dynamic> json) =>
_$$ConnectionsAndKeywordsImplFromJson(json);
final List<Connection> _connections;
@override
@JsonKey()
@@ -1413,64 +1411,62 @@ class _$ConnectionsAndKeywordsImpl implements _ConnectionsAndKeywords {
return EqualUnmodifiableListView(_keywords);
}
@override
@JsonKey()
final String query;
@override
String toString() {
return 'ConnectionsAndKeywords(connections: $connections, keywords: $keywords)';
return 'ConnectionsState(connections: $connections, keywords: $keywords, query: $query)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ConnectionsAndKeywordsImpl &&
other is _$ConnectionsStateImpl &&
const DeepCollectionEquality()
.equals(other._connections, _connections) &&
const DeepCollectionEquality().equals(other._keywords, _keywords));
const DeepCollectionEquality().equals(other._keywords, _keywords) &&
(identical(other.query, query) || other.query == query));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_connections),
const DeepCollectionEquality().hash(_keywords));
const DeepCollectionEquality().hash(_keywords),
query);
/// Create a copy of ConnectionsAndKeywords
/// Create a copy of ConnectionsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ConnectionsAndKeywordsImplCopyWith<_$ConnectionsAndKeywordsImpl>
get copyWith => __$$ConnectionsAndKeywordsImplCopyWithImpl<
_$ConnectionsAndKeywordsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ConnectionsAndKeywordsImplToJson(
this,
);
}
_$$ConnectionsStateImplCopyWith<_$ConnectionsStateImpl> get copyWith =>
__$$ConnectionsStateImplCopyWithImpl<_$ConnectionsStateImpl>(
this, _$identity);
}
abstract class _ConnectionsAndKeywords implements ConnectionsAndKeywords {
const factory _ConnectionsAndKeywords(
abstract class _ConnectionsState implements ConnectionsState {
const factory _ConnectionsState(
{final List<Connection> connections,
final List<String> keywords}) = _$ConnectionsAndKeywordsImpl;
factory _ConnectionsAndKeywords.fromJson(Map<String, dynamic> json) =
_$ConnectionsAndKeywordsImpl.fromJson;
final List<String> keywords,
final String query}) = _$ConnectionsStateImpl;
@override
List<Connection> get connections;
@override
List<String> get keywords;
@override
String get query;
/// Create a copy of ConnectionsAndKeywords
/// Create a copy of ConnectionsState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ConnectionsAndKeywordsImplCopyWith<_$ConnectionsAndKeywordsImpl>
get copyWith => throw _privateConstructorUsedError;
_$$ConnectionsStateImplCopyWith<_$ConnectionsStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
DAV _$DAVFromJson(Map<String, dynamic> json) {

View File

@@ -29,7 +29,7 @@ _$PackageImpl _$$PackageImplFromJson(Map<String, dynamic> json) =>
packageName: json['packageName'] as String,
label: json['label'] as String,
isSystem: json['isSystem'] as bool,
firstInstallTime: (json['firstInstallTime'] as num).toInt(),
lastUpdateTime: (json['lastUpdateTime'] as num).toInt(),
);
Map<String, dynamic> _$$PackageImplToJson(_$PackageImpl instance) =>
@@ -37,7 +37,7 @@ Map<String, dynamic> _$$PackageImplToJson(_$PackageImpl instance) =>
'packageName': instance.packageName,
'label': instance.label,
'isSystem': instance.isSystem,
'firstInstallTime': instance.firstInstallTime,
'lastUpdateTime': instance.lastUpdateTime,
};
_$MetadataImpl _$$MetadataImplFromJson(Map<String, dynamic> json) =>
@@ -87,46 +87,6 @@ Map<String, dynamic> _$$ConnectionImplToJson(_$ConnectionImpl instance) =>
'chains': instance.chains,
};
_$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,
};
_$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,
};
_$DAVImpl _$$DAVImplFromJson(Map<String, dynamic> json) => _$DAVImpl(
uri: json['uri'] as String,
user: json['user'] as String,

View File

@@ -310,3 +310,188 @@ abstract class _CommonMessage implements CommonMessage {
_$$CommonMessageImplCopyWith<_$CommonMessageImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$CommonAppBarState {
List<Widget> get actions => throw _privateConstructorUsedError;
dynamic Function(String)? get onSearch => throw _privateConstructorUsedError;
bool get searching => throw _privateConstructorUsedError;
/// Create a copy of CommonAppBarState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$CommonAppBarStateCopyWith<CommonAppBarState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CommonAppBarStateCopyWith<$Res> {
factory $CommonAppBarStateCopyWith(
CommonAppBarState value, $Res Function(CommonAppBarState) then) =
_$CommonAppBarStateCopyWithImpl<$Res, CommonAppBarState>;
@useResult
$Res call(
{List<Widget> actions,
dynamic Function(String)? onSearch,
bool searching});
}
/// @nodoc
class _$CommonAppBarStateCopyWithImpl<$Res, $Val extends CommonAppBarState>
implements $CommonAppBarStateCopyWith<$Res> {
_$CommonAppBarStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CommonAppBarState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? actions = null,
Object? onSearch = freezed,
Object? searching = null,
}) {
return _then(_value.copyWith(
actions: null == actions
? _value.actions
: actions // ignore: cast_nullable_to_non_nullable
as List<Widget>,
onSearch: freezed == onSearch
? _value.onSearch
: onSearch // ignore: cast_nullable_to_non_nullable
as dynamic Function(String)?,
searching: null == searching
? _value.searching
: searching // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$CommonAppBarStateImplCopyWith<$Res>
implements $CommonAppBarStateCopyWith<$Res> {
factory _$$CommonAppBarStateImplCopyWith(_$CommonAppBarStateImpl value,
$Res Function(_$CommonAppBarStateImpl) then) =
__$$CommonAppBarStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<Widget> actions,
dynamic Function(String)? onSearch,
bool searching});
}
/// @nodoc
class __$$CommonAppBarStateImplCopyWithImpl<$Res>
extends _$CommonAppBarStateCopyWithImpl<$Res, _$CommonAppBarStateImpl>
implements _$$CommonAppBarStateImplCopyWith<$Res> {
__$$CommonAppBarStateImplCopyWithImpl(_$CommonAppBarStateImpl _value,
$Res Function(_$CommonAppBarStateImpl) _then)
: super(_value, _then);
/// Create a copy of CommonAppBarState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? actions = null,
Object? onSearch = freezed,
Object? searching = null,
}) {
return _then(_$CommonAppBarStateImpl(
actions: null == actions
? _value._actions
: actions // ignore: cast_nullable_to_non_nullable
as List<Widget>,
onSearch: freezed == onSearch
? _value.onSearch
: onSearch // ignore: cast_nullable_to_non_nullable
as dynamic Function(String)?,
searching: null == searching
? _value.searching
: searching // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$CommonAppBarStateImpl implements _CommonAppBarState {
const _$CommonAppBarStateImpl(
{final List<Widget> actions = const [],
this.onSearch,
this.searching = false})
: _actions = actions;
final List<Widget> _actions;
@override
@JsonKey()
List<Widget> get actions {
if (_actions is EqualUnmodifiableListView) return _actions;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_actions);
}
@override
final dynamic Function(String)? onSearch;
@override
@JsonKey()
final bool searching;
@override
String toString() {
return 'CommonAppBarState(actions: $actions, onSearch: $onSearch, searching: $searching)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CommonAppBarStateImpl &&
const DeepCollectionEquality().equals(other._actions, _actions) &&
(identical(other.onSearch, onSearch) ||
other.onSearch == onSearch) &&
(identical(other.searching, searching) ||
other.searching == searching));
}
@override
int get hashCode => Object.hash(runtimeType,
const DeepCollectionEquality().hash(_actions), onSearch, searching);
/// Create a copy of CommonAppBarState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CommonAppBarStateImplCopyWith<_$CommonAppBarStateImpl> get copyWith =>
__$$CommonAppBarStateImplCopyWithImpl<_$CommonAppBarStateImpl>(
this, _$identity);
}
abstract class _CommonAppBarState implements CommonAppBarState {
const factory _CommonAppBarState(
{final List<Widget> actions,
final dynamic Function(String)? onSearch,
final bool searching}) = _$CommonAppBarStateImpl;
@override
List<Widget> get actions;
@override
dynamic Function(String)? get onSearch;
@override
bool get searching;
/// Create a copy of CommonAppBarState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CommonAppBarStateImplCopyWith<_$CommonAppBarStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -164,7 +164,7 @@ extension PackageListSelectorStateExt on PackageListSelectorState {
other.getPinyin(b.label),
),
AccessSortType.time =>
a.firstInstallTime.compareTo(b.firstInstallTime),
b.lastUpdateTime.compareTo(a.lastUpdateTime),
};
},
).sorted(

View File

@@ -1,3 +1,4 @@
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/widget.freezed.dart';
@@ -17,3 +18,12 @@ class CommonMessage with _$CommonMessage {
@Default(Duration(seconds: 3)) Duration duration,
}) = _CommonMessage;
}
@freezed
class CommonAppBarState with _$CommonAppBarState {
const factory CommonAppBarState({
@Default([]) List<Widget> actions,
Function(String)? onSearch,
@Default(false) bool searching,
}) = _CommonAppBarState;
}

View File

@@ -21,7 +21,10 @@ class HomePage extends StatelessWidget {
final currentIndex = index == -1 ? 0 : index;
if (globalState.pageController != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.toPage(currentIndex, hasAnimate: true);
globalState.appController.toPage(
currentIndex,
hasAnimate: true,
);
});
} else {
globalState.pageController = PageController(
@@ -152,71 +155,69 @@ class CommonNavigationBar extends StatelessWidget {
}
return Material(
color: context.colorScheme.surfaceContainer,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 16,
),
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: IntrinsicHeight(
child: Selector<Config, bool>(
selector: (_, config) => config.appSetting.showLabel,
builder: (_, showLabel, __) {
return NavigationRail(
backgroundColor: context.colorScheme.surfaceContainer,
selectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
unselectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
selectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
unselectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: IntrinsicHeight(
child: Selector<Config, bool>(
selector: (_, config) => config.appSetting.showLabel,
builder: (_, showLabel, __) {
return NavigationRail(
backgroundColor: context.colorScheme.surfaceContainer,
selectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
unselectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
selectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
unselectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
)
.toList(),
onDestinationSelected: globalState.appController.toPage,
extended: false,
selectedIndex: currentIndex,
labelType: showLabel
? NavigationRailLabelType.all
: NavigationRailLabelType.none,
);
},
),
),
)
.toList(),
onDestinationSelected: globalState.appController.toPage,
extended: false,
selectedIndex: currentIndex,
labelType: showLabel
? NavigationRailLabelType.all
: NavigationRailLabelType.none,
);
},
),
),
),
const SizedBox(
height: 16,
),
IconButton(
onPressed: () {
final config = globalState.appController.config;
final appSetting = config.appSetting;
config.appSetting = appSetting.copyWith(
showLabel: !appSetting.showLabel,
);
},
icon: const Icon(Icons.menu),
)
],
),
),
const SizedBox(
height: 16,
),
IconButton(
onPressed: () {
final config = globalState.appController.config;
final appSetting = config.appSetting;
config.appSetting = appSetting.copyWith(
showLabel: !appSetting.showLabel,
);
},
icon: const Icon(Icons.menu),
),
const SizedBox(
height: 16,
),
],
),
);
}

View File

@@ -20,10 +20,11 @@ class CommonChip extends StatelessWidget {
if (type == ChipType.delete) {
return Chip(
avatar: avatar,
labelPadding:const EdgeInsets.symmetric(
labelPadding: const EdgeInsets.symmetric(
vertical: 0,
horizontal: 4,
),
clipBehavior: Clip.antiAlias,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onDeleted: onPressed ?? () {},
side:
@@ -35,7 +36,8 @@ class CommonChip extends StatelessWidget {
return ActionChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
avatar: avatar,
labelPadding:const EdgeInsets.symmetric(
clipBehavior: Clip.antiAlias,
labelPadding: const EdgeInsets.symmetric(
vertical: 0,
horizontal: 4,
),

View File

@@ -1,167 +0,0 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'chip.dart';
import 'list.dart';
class ConnectionItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClick;
final Widget? trailing;
const ConnectionItem({
super.key,
required this.connection,
this.onClick,
this.trailing,
});
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) {
if (!Platform.isAndroid) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
),
trailing: trailing,
);
}
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, value, child) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: value
? GestureDetector(
onTap: () {
if (onClick == null) return;
final process = connection.metadata.process;
if(process.isEmpty) return;
onClick!(process);
},
child: 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: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
),
trailing: trailing,
);
},
);
}
}

View File

@@ -178,10 +178,6 @@ class CommonPopupMenu extends StatelessWidget {
? context.colorScheme.error
: context.colorScheme.onSurfaceVariant;
return InkWell(
hoverColor:
isDanger ? context.colorScheme.errorContainer.withOpacity(0.3) : null,
splashColor:
isDanger ? context.colorScheme.errorContainer.withOpacity(0.4) : null,
onTap: () {
Navigator.of(context).pop();
item.onPressed();

View File

@@ -1,9 +1,13 @@
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/fade_box.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../enum/enum.dart';
import 'chip.dart';
class CommonScaffold extends StatefulWidget {
final Widget body;
final Widget? bottomNavigationBar;
@@ -47,14 +51,32 @@ class CommonScaffold extends StatefulWidget {
}
class CommonScaffoldState extends State<CommonScaffold> {
final ValueNotifier<List<Widget>> _actions = ValueNotifier([]);
final ValueNotifier<CommonAppBarState> _appBarState =
ValueNotifier(CommonAppBarState());
final ValueNotifier<Widget?> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<List<String>> _keywordsNotifier = ValueNotifier([]);
final ValueNotifier<bool> _loading = ValueNotifier(false);
final _textController = TextEditingController();
Function(List<String>)? _onKeywordsUpdate;
Widget? get _sideNavigationBar => widget.sideNavigationBar;
set actions(List<Widget> actions) {
if (_actions.value != actions) {
_actions.value = actions;
}
_appBarState.value = _appBarState.value.copyWith(actions: actions);
}
set onSearch(Function(String)? onSearch) {
_appBarState.value = _appBarState.value.copyWith(onSearch: onSearch);
}
set onKeywordsUpdate(Function(List<String>)? onKeywordsUpdate) {
_onKeywordsUpdate = onKeywordsUpdate;
}
set _searching(bool searching) {
_appBarState.value = _appBarState.value.copyWith(searching: searching);
}
set floatingActionButton(Widget? floatingActionButton) {
@@ -63,6 +85,28 @@ class CommonScaffoldState extends State<CommonScaffold> {
}
}
ThemeData _appBarTheme(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
return theme.copyWith(
appBarTheme: AppBarTheme(
systemOverlayStyle: colorScheme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
backgroundColor: colorScheme.brightness == Brightness.dark
? Colors.grey[900]
: Colors.white,
iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
titleTextStyle: theme.textTheme.titleLarge,
toolbarTextStyle: theme.textTheme.bodyMedium,
),
inputDecorationTheme: InputDecorationTheme(
hintStyle: theme.inputDecorationTheme.hintStyle,
border: InputBorder.none,
),
);
}
Future<T?> loadingRun<T>(
Future<T> Function() futureFunction, {
String? title,
@@ -84,9 +128,31 @@ class CommonScaffoldState extends State<CommonScaffold> {
}
}
_handleClearInput() {
_textController.text = "";
if (_appBarState.value.onSearch != null) {
_appBarState.value.onSearch!("");
}
}
_handleClear() {
if (_textController.text.isNotEmpty) {
_handleClearInput();
return;
}
_searching = false;
}
_handleExitSearching() {
_handleClearInput();
_searching = false;
}
@override
void dispose() {
_actions.dispose();
_appBarState.dispose();
_textController.dispose();
_floatingActionButton.dispose();
super.dispose();
}
@@ -95,59 +161,172 @@ class CommonScaffoldState extends State<CommonScaffold> {
void didUpdateWidget(CommonScaffold oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) {
_actions.value = [];
_appBarState.value = CommonAppBarState();
_floatingActionButton.value = null;
_textController.text = "";
_keywordsNotifier.value = [];
_onKeywordsUpdate = null;
}
}
Widget? get _sideNavigationBar => widget.sideNavigationBar;
addKeyword(String keyword) {
final isContains = _keywordsNotifier.value.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(_keywordsNotifier.value)..add(keyword);
_keywordsNotifier.value = keywords;
}
Widget get body => SafeArea(child: widget.body);
_deleteKeyword(String keyword) {
final isContains = _keywordsNotifier.value.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(_keywordsNotifier.value)
..remove(keyword);
_keywordsNotifier.value = keywords;
}
@override
Widget build(BuildContext context) {
final body = SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ValueListenableBuilder(
valueListenable: _keywordsNotifier,
builder: (_, keywords, __) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_onKeywordsUpdate != null) {
_onKeywordsUpdate!(keywords);
}
});
if (keywords.isEmpty) {
return SizedBox();
}
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final keyword in keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
);
},
),
Expanded(
child: widget.body,
),
],
),
);
final scaffold = Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(
alignment: Alignment.bottomCenter,
children: [
ValueListenableBuilder<List<Widget>>(
valueListenable: _actions,
builder: (_, actions, __) {
final realActions =
actions.isNotEmpty ? actions : widget.actions ?? [];
return AppBar(
ValueListenableBuilder<CommonAppBarState>(
valueListenable: _appBarState,
builder: (_, state, __) {
final realActions = [
if (state.onSearch != null)
IconButton(
onPressed: () {
_searching = true;
},
icon: Icon(Icons.search),
),
...state.actions.isNotEmpty
? state.actions
: widget.actions ?? []
];
final appBar = AppBar(
centerTitle: false,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: widget.bottomNavigationBar != null
? context.colorScheme.surfaceContainer
: context.colorScheme.surface,
systemNavigationBarDividerColor: Colors.transparent,
),
automaticallyImplyLeading: widget.automaticallyImplyLeading,
leading: widget.leading,
title: Text(widget.title),
leading: state.searching
? IconButton(
onPressed: _handleExitSearching,
icon: Icon(Icons.arrow_back),
)
: widget.leading,
title: state.searching
? TextField(
autofocus: true,
controller: _textController,
style: context.textTheme.titleLarge,
onChanged: (value) {
if (state.onSearch != null) {
state.onSearch!(value);
}
},
decoration: InputDecoration(
hintText: appLocalizations.search,
),
)
: Text(widget.title),
actions: [
...realActions.separated(
SizedBox(
width: 4,
if (state.searching)
IconButton(
onPressed: _handleClear,
icon: Icon(Icons.close),
)
else
...realActions.separated(
SizedBox(
width: 4,
),
),
),
SizedBox(
width: 8,
)
],
);
return FadeBox(
child: state.searching
? Theme(
data: _appBarTheme(context),
child: PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, __) {
if (didPop) {
return;
}
if (state.searching) {
_handleExitSearching();
return;
}
Navigator.of(context).pop();
},
child: appBar,
),
)
: appBar,
);
},
),
ValueListenableBuilder(
@@ -179,12 +358,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
_sideNavigationBar!,
Expanded(
flex: 1,
child: Material(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: scaffold,
),
),
child: scaffold,
),
],
)

25
lib/widgets/scroll.dart Normal file
View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class CommonScrollBar extends StatelessWidget {
final ScrollController controller;
final Widget child;
const CommonScrollBar({
super.key,
required this.child,
required this.controller,
});
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
thumbVisibility: true,
trackVisibility: true,
thickness: 8,
radius: const Radius.circular(8),
interactive: true,
child: child,
);
}
}

20
lib/widgets/view.dart Normal file
View File

@@ -0,0 +1,20 @@
import 'package:flutter/cupertino.dart';
class CommonView extends StatefulWidget {
final List<Widget> actions;
const CommonView({
super.key,
required this.actions,
});
@override
State<CommonView> createState() => _CommonViewState();
}
class _CommonViewState extends State<CommonView> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@@ -4,7 +4,6 @@ export 'builder.dart';
export 'card.dart';
export 'chip.dart';
export 'color_scheme_box.dart';
export 'connection_item.dart';
export 'disabled_mask.dart';
export 'fade_box.dart';
export 'float_layout.dart';
@@ -27,3 +26,4 @@ export 'super_grid.dart';
export 'donut_chart.dart';
export 'activate_box.dart';
export 'wave.dart';
export 'scroll.dart';

View File

@@ -210,7 +210,7 @@ class Proxy extends ProxyPlatform {
[
"-setproxybypassdomains",
dev,
bypassDomain.join(" "),
bypassDomain.join(","),
],
),
]);

View File

@@ -166,14 +166,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
@@ -570,14 +562,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
image:
dependency: "direct main"
description:
name: image
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev"
source: hosted
version: "4.3.0"
image_picker:
dependency: "direct main"
description:
@@ -1061,10 +1045,10 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
version: "2.5.1"
shared_preferences_android:
dependency: transitive
description:
@@ -1501,14 +1485,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.1"
zxing2:
dependency: "direct main"
description:
name: zxing2
sha256: "6cf995abd3c86f01ba882968dedffa7bc130185e382f2300239d2e857fc7912c"
url: "https://pub.dev"
source: hosted
version: "0.2.3"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"

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.74+202502031
version: 0.8.75+202502091
environment:
sdk: '>=3.1.0 <4.0.0'
@@ -13,7 +13,7 @@ dependencies:
intl: ^0.19.0
path_provider: ^2.1.0
path: ^1.9.0
shared_preferences: ^2.2.0
shared_preferences: ^2.5.1
provider: ^6.0.5
window_manager: ^0.4.3
dynamic_color: ^1.7.0
@@ -35,8 +35,6 @@ dependencies:
url_launcher: ^6.2.6
freezed_annotation: ^2.4.1
image_picker: ^1.1.2
zxing2: ^0.2.3
image: ^4.1.7
webdav_client: ^1.2.2
dio: ^5.4.3+1
win32: ^5.5.1