Compare commits

...

3 Commits

Author SHA1 Message Date
chen08209
48391ecbbf Fix scroll physics error 2025-02-09 16:51:30 +08:00
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 3083 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,95 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
); );
} }
} }
class NextClampingScrollPhysics extends ClampingScrollPhysics {
const NextClampingScrollPhysics({super.parent});
@override
NextClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return NextClampingScrollPhysics(parent: buildParent(ancestor));
}
@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,
});
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.76+202502092
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