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 ## v0.8.73
- Update popup menu - Update popup menu

View File

@@ -4,5 +4,5 @@ data class Package(
val packageName: String, val packageName: String,
val label: String, val label: String,
val isSystem: Boolean, 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.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@@ -302,7 +301,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
packageName = it.packageName, packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(), label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1, isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
firstInstallTime = it.firstInstallTime lastUpdateTime = it.lastUpdateTime
) )
}?.let { packages.addAll(it) } }?.let { packages.addAll(it) }
return packages return packages

View File

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

View File

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

View File

@@ -34,4 +34,5 @@ export 'text.dart';
export 'tray.dart'; export 'tray.dart';
export 'window.dart'; export 'window.dart';
export 'windows.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; ColorScheme get colorScheme => Theme.of(this).colorScheme;
TextTheme get textTheme => Theme.of(this).textTheme; 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'; import 'dart:async';
class Debouncer { class Debouncer {
Map<dynamic, Timer> operators = {}; final Map<dynamic, Timer> _operations = {};
call( call(
dynamic tag, dynamic tag,
@@ -9,14 +9,15 @@ class Debouncer {
List<dynamic>? args, List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600), Duration duration = const Duration(milliseconds: 600),
}) { }) {
final timer = operators[tag]; final timer = _operations[tag];
if (timer != null) { if (timer != null) {
timer.cancel(); timer.cancel();
} }
operators[tag] = Timer( _operations[tag] = Timer(
duration, duration,
() { () {
operators.remove(tag); _operations[tag]?.cancel();
_operations.remove(tag);
Function.apply( Function.apply(
func, func,
args, args,
@@ -26,8 +27,43 @@ class Debouncer {
} }
cancel(dynamic tag) { 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 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> { extension ListExtension<T> on List<T> {
List<T> intersection(List<T> list) { List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList(); return where((item) => list.contains(item)).toList();
@@ -17,8 +69,8 @@ extension ListExtension<T> on List<T> {
} }
List<T> safeSublist(int start) { List<T> safeSublist(int start) {
if(start <= 0) return this; if (start <= 0) return this;
if(start > length) return []; if (start > length) return [];
return sublist(start); return sublist(start);
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:lpinyin/lpinyin.dart'; import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart';
class Other { class Other {
Color? getDelayColor(int? delay) { 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 { String get uuidV4 {
final Random random = Random(); final Random random = Random();
final bytes = List.generate(16, (_) => random.nextInt(256)); 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) { String? getFileNameForDisposition(String? disposition) {
if (disposition == null) return null; if (disposition == null) return null;
final parseValue = HeaderValue.parse(disposition); final parseValue = HeaderValue.parse(disposition);

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
@@ -15,6 +16,8 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
}; };
} }
class BaseScrollBehavior2 extends ScrollBehavior {}
class HiddenBarScrollBehavior extends BaseScrollBehavior { class HiddenBarScrollBehavior extends BaseScrollBehavior {
@override @override
Widget buildScrollbar( 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 { _handlePreference() async {
final isDisclaimerAccepted = await handlerDisclaimer(); if (await preferences.isInit) {
if (!isDisclaimerAccepted) { return;
handleExit();
} }
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( await globalState.initCore(
appState: appState, appState: appState,
clashConfig: clashConfig, clashConfig: clashConfig,
@@ -473,11 +489,15 @@ class AppController {
false; false;
} }
Future<bool> handlerDisclaimer() async { _handlerDisclaimer() async {
if (config.appSetting.disclaimerAccepted) { if (config.appSetting.disclaimerAccepted) {
return true; return;
} }
return showDisclaimer(); final isDisclaimerAccepted = await showDisclaimer();
if (!isDisclaimerAccepted) {
await handleExit();
}
return;
} }
addProfileFormURL(String url) async { addProfileFormURL(String url) async {
@@ -673,8 +693,8 @@ class AppController {
} }
Future<List<int>> backupData() async { Future<List<int>> backupData() async {
final homeDirPath = await appPath.getHomeDirPath(); final homeDirPath = await appPath.homeDirPath;
final profilesPath = await appPath.getProfilesPath(); final profilesPath = await appPath.profilesPath;
final configJson = config.toJson(); final configJson = config.toJson();
final clashConfigJson = clashConfig.toJson(); final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async { return Isolate.run<List<int>>(() async {
@@ -705,7 +725,7 @@ class AppController {
final zipDecoder = ZipDecoder(); final zipDecoder = ZipDecoder();
return zipDecoder.decodeBytes(data); return zipDecoder.decodeBytes(data);
}); });
final homeDirPath = await appPath.getHomeDirPath(); final homeDirPath = await appPath.homeDirPath;
final configs = final configs =
archive.files.where((item) => item.name.endsWith(".json")).toList(); archive.files.where((item) => item.name.endsWith(".json")).toList();
final profiles = final profiles =

View File

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

View File

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

View File

@@ -206,9 +206,7 @@ class BypassDomainItem extends StatelessWidget {
_initActions(BuildContext context) { _initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () async { onPressed: () async {
final res = await globalState.showMessage( final res = await globalState.showMessage(
@@ -378,9 +376,7 @@ class NetworkListView extends StatelessWidget {
_initActions(BuildContext context) { _initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () async { onPressed: () async {
final res = await globalState.showMessage( 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 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -23,8 +24,7 @@ class _DashboardFragmentState extends State<DashboardFragment> {
return; return;
} }
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = final commonScaffoldState = context.commonScaffoldState;
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = const StartButton(); commonScaffoldState?.floatingActionButton = const StartButton();
commonScaffoldState?.actions = [ commonScaffoldState?.actions = [
ValueListenableBuilder( ValueListenableBuilder(

View File

@@ -3,11 +3,11 @@ export 'dashboard/dashboard.dart';
export 'tools.dart'; export 'tools.dart';
export 'profiles/profiles.dart'; export 'profiles/profiles.dart';
export 'logs.dart'; export 'logs.dart';
export 'connections.dart';
export 'access.dart'; export 'access.dart';
export 'config/config.dart'; export 'config/config.dart';
export 'application_setting.dart'; export 'application_setting.dart';
export 'about.dart'; export 'about.dart';
export 'backup_and_recovery.dart'; export 'backup_and_recovery.dart';
export 'resources.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 'dart:async';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../widgets/widgets.dart'; import '../widgets/widgets.dart';
double _preOffset = 0;
class LogsFragment extends StatefulWidget { class LogsFragment extends StatefulWidget {
const LogsFragment({super.key}); const LogsFragment({super.key});
@@ -14,48 +18,66 @@ class LogsFragment extends StatefulWidget {
State<LogsFragment> createState() => _LogsFragmentState(); State<LogsFragment> createState() => _LogsFragmentState();
} }
class _LogsFragmentState extends State<LogsFragment> { class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
final logsNotifier = ValueNotifier<LogsAndKeywords>(const LogsAndKeywords()); final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
final scrollController = ScrollController( final _scrollController = ScrollController(
keepScrollOffset: false, initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
); );
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0;
Timer? timer; List<Log> _logs = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { final appController = globalState.appController;
final appFlowingState = globalState.appController.appFlowingState; final appFlowingState = appController.appFlowingState;
logsNotifier.value = _logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logsNotifier.value.copyWith(logs: appFlowingState.logs); 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,
);
}
});
});
} }
@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 @override
void dispose() { void dispose() {
timer?.cancel(); _logsStateNotifier.dispose();
logsNotifier.dispose(); _scrollController.dispose();
scrollController.dispose(); _cacheDynamicHeightMap.clear();
timer = null;
super.dispose(); super.dispose();
} }
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear();
}
}
_handleExport() async { _handleExport() async {
final commonScaffoldState = context.commonScaffoldState; final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>( final res = await commonScaffoldState?.loadingRun<bool>(
@@ -73,293 +95,151 @@ class _LogsFragmentState extends State<LogsFragment> {
_initActions() { _initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = super.initViewState();
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,
),
),
];
}); });
} }
_addKeyword(String keyword) { double _calcCacheHeight(String text) {
final isContains = logsNotifier.value.keywords.contains(keyword); final cacheHeight = _cacheDynamicHeightMap.get(text);
if (isContains) return; if (cacheHeight != null) {
final keywords = List<String>.from(logsNotifier.value.keywords) return cacheHeight;
..add(keyword); }
logsNotifier.value = logsNotifier.value.copyWith( final size = globalState.measure.computeTextSize(
keywords: keywords, Text(
text,
style: globalState.appController.context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
); );
_cacheDynamicHeightMap.put(text, size.height);
return size.height;
} }
_deleteKeyword(String keyword) { double _getItemHeight(Log log) {
final isContains = logsNotifier.value.keywords.contains(keyword); final measure = globalState.measure;
if (!isContains) return; final bodySmallHeight = measure.bodySmallHeight;
final keywords = List<String>.from(logsNotifier.value.keywords) final bodyMediumHeight = measure.bodyMediumHeight;
..remove(keyword); final height = _calcCacheHeight(log.payload ?? "");
logsNotifier.value = logsNotifier.value.copyWith( return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
keywords: keywords, }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<AppState, bool?>( return LayoutBuilder(
selector: (_, appState) => builder: (_, constraints) {
appState.currentLabel == 'logs' || _handleTryClearCache(constraints.maxWidth - 40);
appState.viewMode == ViewMode.mobile && return Selector<AppState, bool?>(
appState.currentLabel == "tools", selector: (_, appState) =>
builder: (_, isCurrent, child) { appState.currentLabel == 'logs' ||
if (isCurrent == null || isCurrent) { appState.viewMode == ViewMode.mobile &&
_initActions(); appState.currentLabel == "tools",
} builder: (_, isCurrent, child) {
return child!; if (isCurrent == null || isCurrent) {
}, _initActions();
child: ValueListenableBuilder<LogsAndKeywords>( }
valueListenable: logsNotifier, return child!;
builder: (_, state, __) { },
final logs = state.filteredLogs; child: _wrapLogsUpdate(
if (logs.isEmpty) { Align(
return NullStatus( alignment: Alignment.topCenter,
label: appLocalizations.nullLogsDesc, child: ValueListenableBuilder<LogsState>(
); valueListenable: _logsStateNotifier,
} builder: (_, state, __) {
final reversedLogs = logs.reversed.toList(); final logs = state.list;
final logWidgets = reversedLogs if (logs.isEmpty) {
.map<Widget>( return NullStatus(
(log) => LogItem( label: appLocalizations.nullLogsDesc,
key: Key(log.dateTime.toString()), );
log: log, }
onClick: _addKeyword, final items = logs
), .map<Widget>(
) (log) => LogItem(
.separated( key: Key(log.dateTime.toString()),
const Divider( log: log,
height: 0, onClick: (value) {
), context.commonScaffoldState?.addKeyword(value);
)
.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);
}, },
), ),
], )
), .separated(
), const Divider(
Expanded( height: 0,
child: LayoutBuilder( ),
builder: (_, constraints) { )
return ScrollConfiguration( .toList();
behavior: ShowBarScrollBehavior(), return NotificationListener<ScrollEndNotification>(
child: ListView.builder( onNotification: (details) {
controller: scrollController, _preOffset = details.metrics.pixels;
itemExtentBuilder: (index, __) { return false;
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);
}, },
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 Log log;
final Function(String)? onClick; final Function(String)? onClick;
@@ -369,14 +249,8 @@ class LogItem extends StatefulWidget {
this.onClick, this.onClick,
}); });
@override
State<LogItem> createState() => _LogItemState();
}
class _LogItemState extends State<LogItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final log = widget.log;
return ListItem( return ListItem(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
@@ -384,14 +258,16 @@ class _LogItemState extends State<LogItem> {
), ),
title: SelectableText( title: SelectableText(
log.payload ?? '', log.payload ?? '',
style: context.textTheme.bodyLarge,
), ),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SelectableText( SelectableText(
"${log.dateTime}", "${log.dateTime}",
style: context.textTheme.bodySmall style: context.textTheme.bodySmall?.copyWith(
?.copyWith(color: context.colorScheme.primary), color: context.colorScheme.primary,
),
), ),
const SizedBox( const SizedBox(
height: 8, height: 8,
@@ -400,8 +276,8 @@ class _LogItemState extends State<LogItem> {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: CommonChip( child: CommonChip(
onPressed: () { onPressed: () {
if (widget.onClick == null) return; if (onClick == null) return;
widget.onClick!(log.logLevel.name); onClick!(log.logLevel.name);
}, },
label: 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( WidgetsBinding.instance.addPostFrameCallback(
(_) { (_) {
if (!mounted) return; if (!mounted) return;
final commonScaffoldState = final commonScaffoldState = context.commonScaffoldState;
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [ commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () {

View File

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

View File

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

View File

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

View File

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

View File

@@ -320,9 +320,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
return; return;
} }
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState?.floatingActionButton = DelayTestButton(
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DelayTestButton(
onClick: () async { onClick: () async {
await _delayTest(); 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 { Future<FileInfo> _getGeoFileLastModified(String fileName) async {
final homePath = await appPath.getHomeDirPath(); final homePath = await appPath.homeDirPath;
final file = File(join(homePath, fileName)); final file = File(join(homePath, fileName));
final lastModified = await file.lastModified(); final lastModified = await file.lastModified();
final size = await file.length(); final size = await file.length();
@@ -183,7 +183,12 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
} }
_handleUpdateGeoDataItem() async { _handleUpdateGeoDataItem() async {
await globalState.safeRun<void>(updateGeoDateItem); await globalState.safeRun<void>(
() async {
await updateGeoDateItem();
},
silence: false,
);
setState(() {}); setState(() {});
} }
@@ -196,6 +201,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
geoType: geoItem.label, geoType: geoItem.label,
), ),
); );
print(message);
if (message.isNotEmpty) throw message; if (message.isNotEmpty) throw message;
} catch (e) { } catch (e) {
isUpdating.value = false; isUpdating.value = false;

View File

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

View File

@@ -341,5 +341,6 @@
"nullProxies": "No proxies", "nullProxies": "No proxies",
"copySuccess": "Copy success", "copySuccess": "Copy success",
"copyLink": "Copy link", "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": "暂无代理", "nullProxies": "暂无代理",
"copySuccess": "复制成功", "copySuccess": "复制成功",
"copyLink": "复制链接", "copyLink": "复制链接",
"exportFile": "导出文件" "exportFile": "导出文件",
"cacheCorrupt": "缓存已损坏,是否清空?"
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -310,3 +310,188 @@ abstract class _CommonMessage implements CommonMessage {
_$$CommonMessageImplCopyWith<_$CommonMessageImpl> get copyWith => _$$CommonMessageImplCopyWith<_$CommonMessageImpl> get copyWith =>
throw _privateConstructorUsedError; 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), other.getPinyin(b.label),
), ),
AccessSortType.time => AccessSortType.time =>
a.firstInstallTime.compareTo(b.firstInstallTime), b.lastUpdateTime.compareTo(a.lastUpdateTime),
}; };
}, },
).sorted( ).sorted(

View File

@@ -1,3 +1,4 @@
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/widget.freezed.dart'; part 'generated/widget.freezed.dart';
@@ -17,3 +18,12 @@ class CommonMessage with _$CommonMessage {
@Default(Duration(seconds: 3)) Duration duration, @Default(Duration(seconds: 3)) Duration duration,
}) = _CommonMessage; }) = _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; final currentIndex = index == -1 ? 0 : index;
if (globalState.pageController != null) { if (globalState.pageController != null) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.toPage(currentIndex, hasAnimate: true); globalState.appController.toPage(
currentIndex,
hasAnimate: true,
);
}); });
} else { } else {
globalState.pageController = PageController( globalState.pageController = PageController(
@@ -152,71 +155,69 @@ class CommonNavigationBar extends StatelessWidget {
} }
return Material( return Material(
color: context.colorScheme.surfaceContainer, color: context.colorScheme.surfaceContainer,
child: Padding( child: Column(
padding: const EdgeInsets.symmetric( children: [
vertical: 16, Expanded(
), child: SingleChildScrollView(
child: Column( child: IntrinsicHeight(
children: [ child: Selector<Config, bool>(
Expanded( selector: (_, config) => config.appSetting.showLabel,
child: SingleChildScrollView( builder: (_, showLabel, __) {
child: IntrinsicHeight( return NavigationRail(
child: Selector<Config, bool>( backgroundColor: context.colorScheme.surfaceContainer,
selector: (_, config) => config.appSetting.showLabel, selectedIconTheme: IconThemeData(
builder: (_, showLabel, __) { color: context.colorScheme.onSurfaceVariant,
return NavigationRail( ),
backgroundColor: context.colorScheme.surfaceContainer, unselectedIconTheme: IconThemeData(
selectedIconTheme: IconThemeData( color: context.colorScheme.onSurfaceVariant,
color: context.colorScheme.onSurfaceVariant, ),
), selectedLabelTextStyle:
unselectedIconTheme: IconThemeData( context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurfaceVariant, color: context.colorScheme.onSurface,
), ),
selectedLabelTextStyle: unselectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith( context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface, color: context.colorScheme.onSurface,
), ),
unselectedLabelTextStyle: destinations: navigationItems
context.textTheme.labelLarge!.copyWith( .map(
color: context.colorScheme.onSurface, (e) => NavigationRailDestination(
), icon: e.icon,
destinations: navigationItems label: Text(
.map( Intl.message(e.label),
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
), ),
) ),
.toList(), )
onDestinationSelected: globalState.appController.toPage, .toList(),
extended: false, onDestinationSelected: globalState.appController.toPage,
selectedIndex: currentIndex, extended: false,
labelType: showLabel selectedIndex: currentIndex,
? NavigationRailLabelType.all labelType: showLabel
: NavigationRailLabelType.none, ? NavigationRailLabelType.all
); : NavigationRailLabelType.none,
}, );
), },
), ),
), ),
), ),
const SizedBox( ),
height: 16, const SizedBox(
), height: 16,
IconButton( ),
onPressed: () { IconButton(
final config = globalState.appController.config; onPressed: () {
final appSetting = config.appSetting; final config = globalState.appController.config;
config.appSetting = appSetting.copyWith( final appSetting = config.appSetting;
showLabel: !appSetting.showLabel, config.appSetting = appSetting.copyWith(
); showLabel: !appSetting.showLabel,
}, );
icon: const Icon(Icons.menu), },
) icon: const Icon(Icons.menu),
], ),
), const SizedBox(
height: 16,
),
],
), ),
); );
} }

View File

@@ -20,10 +20,11 @@ class CommonChip extends StatelessWidget {
if (type == ChipType.delete) { if (type == ChipType.delete) {
return Chip( return Chip(
avatar: avatar, avatar: avatar,
labelPadding:const EdgeInsets.symmetric( labelPadding: const EdgeInsets.symmetric(
vertical: 0, vertical: 0,
horizontal: 4, horizontal: 4,
), ),
clipBehavior: Clip.antiAlias,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onDeleted: onPressed ?? () {}, onDeleted: onPressed ?? () {},
side: side:
@@ -35,7 +36,8 @@ class CommonChip extends StatelessWidget {
return ActionChip( return ActionChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
avatar: avatar, avatar: avatar,
labelPadding:const EdgeInsets.symmetric( clipBehavior: Clip.antiAlias,
labelPadding: const EdgeInsets.symmetric(
vertical: 0, vertical: 0,
horizontal: 4, 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.error
: context.colorScheme.onSurfaceVariant; : context.colorScheme.onSurfaceVariant;
return InkWell( return InkWell(
hoverColor:
isDanger ? context.colorScheme.errorContainer.withOpacity(0.3) : null,
splashColor:
isDanger ? context.colorScheme.errorContainer.withOpacity(0.4) : null,
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
item.onPressed(); item.onPressed();

View File

@@ -1,9 +1,13 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/fade_box.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../enum/enum.dart';
import 'chip.dart';
class CommonScaffold extends StatefulWidget { class CommonScaffold extends StatefulWidget {
final Widget body; final Widget body;
final Widget? bottomNavigationBar; final Widget? bottomNavigationBar;
@@ -47,14 +51,32 @@ class CommonScaffold extends StatefulWidget {
} }
class CommonScaffoldState extends State<CommonScaffold> { 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<Widget?> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<List<String>> _keywordsNotifier = ValueNotifier([]);
final ValueNotifier<bool> _loading = ValueNotifier(false); final ValueNotifier<bool> _loading = ValueNotifier(false);
final _textController = TextEditingController();
Function(List<String>)? _onKeywordsUpdate;
Widget? get _sideNavigationBar => widget.sideNavigationBar;
set actions(List<Widget> actions) { set actions(List<Widget> actions) {
if (_actions.value != actions) { _appBarState.value = _appBarState.value.copyWith(actions: actions);
_actions.value = 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) { 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?> loadingRun<T>(
Future<T> Function() futureFunction, { Future<T> Function() futureFunction, {
String? title, 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 @override
void dispose() { void dispose() {
_actions.dispose(); _appBarState.dispose();
_textController.dispose();
_floatingActionButton.dispose(); _floatingActionButton.dispose();
super.dispose(); super.dispose();
} }
@@ -95,59 +161,172 @@ class CommonScaffoldState extends State<CommonScaffold> {
void didUpdateWidget(CommonScaffold oldWidget) { void didUpdateWidget(CommonScaffold oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) { if (oldWidget.title != widget.title) {
_actions.value = []; _appBarState.value = CommonAppBarState();
_floatingActionButton.value = null; _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 @override
Widget build(BuildContext context) { 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( final scaffold = Scaffold(
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight), preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack( child: Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
ValueListenableBuilder<List<Widget>>( ValueListenableBuilder<CommonAppBarState>(
valueListenable: _actions, valueListenable: _appBarState,
builder: (_, actions, __) { builder: (_, state, __) {
final realActions = final realActions = [
actions.isNotEmpty ? actions : widget.actions ?? []; if (state.onSearch != null)
return AppBar( IconButton(
onPressed: () {
_searching = true;
},
icon: Icon(Icons.search),
),
...state.actions.isNotEmpty
? state.actions
: widget.actions ?? []
];
final appBar = AppBar(
centerTitle: false, centerTitle: false,
systemOverlayStyle: SystemUiOverlayStyle( systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
statusBarIconBrightness: statusBarIconBrightness:
Theme.of(context).brightness == Brightness.dark Theme.of(context).brightness == Brightness.dark
? Brightness.light ? Brightness.light
: Brightness.dark, : Brightness.dark,
systemNavigationBarIconBrightness: systemNavigationBarIconBrightness:
Theme.of(context).brightness == Brightness.dark Theme.of(context).brightness == Brightness.dark
? Brightness.light ? Brightness.light
: Brightness.dark, : Brightness.dark,
systemNavigationBarColor: widget.bottomNavigationBar != null systemNavigationBarColor: widget.bottomNavigationBar != null
? context.colorScheme.surfaceContainer ? context.colorScheme.surfaceContainer
: context.colorScheme.surface, : context.colorScheme.surface,
systemNavigationBarDividerColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent,
), ),
automaticallyImplyLeading: widget.automaticallyImplyLeading, automaticallyImplyLeading: widget.automaticallyImplyLeading,
leading: widget.leading, leading: state.searching
title: Text(widget.title), ? 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: [ actions: [
...realActions.separated( if (state.searching)
SizedBox( IconButton(
width: 4, onPressed: _handleClear,
icon: Icon(Icons.close),
)
else
...realActions.separated(
SizedBox(
width: 4,
),
), ),
),
SizedBox( SizedBox(
width: 8, 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( ValueListenableBuilder(
@@ -179,12 +358,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
_sideNavigationBar!, _sideNavigationBar!,
Expanded( Expanded(
flex: 1, flex: 1,
child: Material( child: scaffold,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
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 'card.dart';
export 'chip.dart'; export 'chip.dart';
export 'color_scheme_box.dart'; export 'color_scheme_box.dart';
export 'connection_item.dart';
export 'disabled_mask.dart'; export 'disabled_mask.dart';
export 'fade_box.dart'; export 'fade_box.dart';
export 'float_layout.dart'; export 'float_layout.dart';
@@ -27,3 +26,4 @@ export 'super_grid.dart';
export 'donut_chart.dart'; export 'donut_chart.dart';
export 'activate_box.dart'; export 'activate_box.dart';
export 'wave.dart'; export 'wave.dart';
export 'scroll.dart';

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.74+202502031 version: 0.8.75+202502091
environment: environment:
sdk: '>=3.1.0 <4.0.0' sdk: '>=3.1.0 <4.0.0'
@@ -13,7 +13,7 @@ dependencies:
intl: ^0.19.0 intl: ^0.19.0
path_provider: ^2.1.0 path_provider: ^2.1.0
path: ^1.9.0 path: ^1.9.0
shared_preferences: ^2.2.0 shared_preferences: ^2.5.1
provider: ^6.0.5 provider: ^6.0.5
window_manager: ^0.4.3 window_manager: ^0.4.3
dynamic_color: ^1.7.0 dynamic_color: ^1.7.0
@@ -35,8 +35,6 @@ dependencies:
url_launcher: ^6.2.6 url_launcher: ^6.2.6
freezed_annotation: ^2.4.1 freezed_annotation: ^2.4.1
image_picker: ^1.1.2 image_picker: ^1.1.2
zxing2: ^0.2.3
image: ^4.1.7
webdav_client: ^1.2.2 webdav_client: ^1.2.2
dio: ^5.4.3+1 dio: ^5.4.3+1
win32: ^5.5.1 win32: ^5.5.1