Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa1b4f424 | ||
|
|
fa67940ec9 | ||
|
|
90bb670442 | ||
|
|
05abf2d56d | ||
|
|
658727dd79 | ||
|
|
f7abf6446c | ||
|
|
5ab4dd0cbd | ||
|
|
35f7279fcb | ||
|
|
86572cc960 | ||
|
|
ee22709d49 | ||
|
|
0a2ad63f38 | ||
|
|
2ec12c9363 | ||
|
|
a3c2dc786c | ||
|
|
7acf9c6db3 | ||
|
|
8074547fb4 | ||
|
|
8a01e04871 | ||
|
|
7ddcdd9828 | ||
|
|
d89ed076fd | ||
|
|
f4c3b06cd5 |
@@ -62,7 +62,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.follow.clash"
|
||||
minSdkVersion 21
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:extractNativeLibs="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:label="FlClash">
|
||||
<activity
|
||||
android:name="com.follow.clash.MainActivity"
|
||||
|
||||
@@ -64,6 +64,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
toast!!.show()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"moveTaskToBack" -> {
|
||||
@@ -150,6 +151,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
val message = call.argument<String>("message")
|
||||
tip(message)
|
||||
result.success(true)
|
||||
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
@@ -396,8 +396,6 @@ func applyConfig(isPatch bool) {
|
||||
if isPatch {
|
||||
patchConfig(cfg.General)
|
||||
} else {
|
||||
executor.Shutdown()
|
||||
runtime.GC()
|
||||
hub.UltraApplyConfig(cfg, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,11 +203,6 @@ func getTotalTraffic() *C.char {
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export resetTraffic
|
||||
func resetTraffic() {
|
||||
statistic.DefaultManager.ResetStatistic()
|
||||
}
|
||||
|
||||
//export asyncTestDelay
|
||||
func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
|
||||
@@ -95,9 +95,7 @@ class ClashCore {
|
||||
final proxiesRaw = clashFFI.getProxies();
|
||||
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
|
||||
return Isolate.run<List<Group>>(() {
|
||||
if(proxiesRawString.isEmpty) return [];
|
||||
final proxies = json.decode(proxiesRawString) as Map;
|
||||
if(proxies.isEmpty) return [];
|
||||
final proxies = json.decode(proxiesRawString);
|
||||
final groupNames = [
|
||||
UsedProxy.GLOBAL.name,
|
||||
...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) {
|
||||
@@ -178,11 +176,9 @@ class ClashCore {
|
||||
);
|
||||
Future.delayed(httpTimeoutDuration + moreDuration, () {
|
||||
receiver.close();
|
||||
if(!completer.isCompleted){
|
||||
completer.complete(
|
||||
Delay(name: proxyName, value: -1),
|
||||
);
|
||||
}
|
||||
completer.complete(
|
||||
Delay(name: proxyName, value: -1),
|
||||
);
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
@@ -203,16 +199,13 @@ class ClashCore {
|
||||
return Traffic.fromMap(trafficMap);
|
||||
}
|
||||
|
||||
|
||||
Traffic getTotalTraffic() {
|
||||
final trafficRaw = clashFFI.getTotalTraffic();
|
||||
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
|
||||
return Traffic.fromMap(trafficMap);
|
||||
}
|
||||
|
||||
void resetTraffic(){
|
||||
clashFFI.resetTraffic();
|
||||
}
|
||||
|
||||
void startLog() {
|
||||
clashFFI.startLog();
|
||||
}
|
||||
@@ -250,10 +243,6 @@ class ClashCore {
|
||||
final connectionsRaw = connectionsData['connections'] as List? ?? [];
|
||||
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
closeConnections(String id) {
|
||||
clashFFI.closeConnection(id.toNativeUtf8().cast());
|
||||
}
|
||||
}
|
||||
|
||||
final clashCore = ClashCore();
|
||||
|
||||
@@ -993,14 +993,6 @@ class ClashFFI {
|
||||
late final _getTotalTraffic =
|
||||
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void resetTraffic() {
|
||||
return _resetTraffic();
|
||||
}
|
||||
|
||||
late final _resetTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('resetTraffic');
|
||||
late final _resetTraffic = _resetTrafficPtr.asFunction<void Function()>();
|
||||
|
||||
void asyncTestDelay(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
int port,
|
||||
|
||||
@@ -29,18 +29,11 @@ class Navigation {
|
||||
label: "profiles",
|
||||
fragment: ProfilesFragment(),
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.view_timeline),
|
||||
label: "requests",
|
||||
fragment: RequestsFragment(),
|
||||
description: "requestsDesc",
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.ballot),
|
||||
label: "connections",
|
||||
fragment: ConnectionsFragment(),
|
||||
description: "connectionsDesc",
|
||||
label: "requests",
|
||||
fragment: RequestFragment(),
|
||||
description: "requestsDesc",
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
|
||||
@@ -8,30 +8,12 @@ import 'constant.dart';
|
||||
|
||||
class AppPath {
|
||||
static AppPath? _instance;
|
||||
Completer<Directory> cacheDir = Completer();
|
||||
|
||||
// Future<Directory> _createDesktopCacheDir() async {
|
||||
// final path = join(dirname(Platform.resolvedExecutable), 'cache');
|
||||
// final dir = Directory(path);
|
||||
// if (await dir.exists()) {
|
||||
// await dir.create(recursive: true);
|
||||
// }
|
||||
// return dir;
|
||||
// }
|
||||
Completer<Directory> applicationSupportDirectoryCompleter = Completer();
|
||||
|
||||
AppPath._internal() {
|
||||
getApplicationSupportDirectory().then((value) {
|
||||
cacheDir.complete(value);
|
||||
});
|
||||
// if (Platform.isAndroid) {
|
||||
// getApplicationSupportDirectory().then((value) {
|
||||
// cacheDir.complete(value);
|
||||
// });
|
||||
// } else {
|
||||
// _createDesktopCacheDir().then((value) {
|
||||
// cacheDir.complete(value);
|
||||
// });
|
||||
// }
|
||||
getApplicationSupportDirectory().then(
|
||||
(value) => applicationSupportDirectoryCompleter.complete(value),
|
||||
);
|
||||
}
|
||||
|
||||
factory AppPath() {
|
||||
@@ -40,12 +22,12 @@ class AppPath {
|
||||
}
|
||||
|
||||
Future<String> getHomeDirPath() async {
|
||||
final directory = await cacheDir.future;
|
||||
final directory = await applicationSupportDirectoryCompleter.future;
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<String> getProfilesPath() async {
|
||||
final directory = await cacheDir.future;
|
||||
final directory = await applicationSupportDirectoryCompleter.future;
|
||||
return join(directory.path, profilesDirectoryName);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,20 @@ import 'package:flutter/material.dart';
|
||||
import 'color.dart';
|
||||
|
||||
extension TextStyleExtension on TextStyle {
|
||||
TextStyle get toLight => copyWith(color: color?.toLight());
|
||||
toLight() {
|
||||
return copyWith(color: color?.toLight());
|
||||
}
|
||||
|
||||
TextStyle get toLighter => copyWith(color: color?.toLighter());
|
||||
toLighter() {
|
||||
return copyWith(color: color?.toLighter());
|
||||
}
|
||||
|
||||
TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500);
|
||||
|
||||
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
|
||||
toSoftBold() {
|
||||
return copyWith(fontWeight: FontWeight.w500);
|
||||
}
|
||||
|
||||
TextStyle get toMinus => copyWith(fontSize: fontSize! - 1);
|
||||
}
|
||||
toBold() {
|
||||
return copyWith(fontWeight: FontWeight.bold);
|
||||
}
|
||||
}
|
||||
@@ -43,9 +43,7 @@ class AppController {
|
||||
];
|
||||
} else {
|
||||
await globalState.stopSystemProxy();
|
||||
clashCore.resetTraffic();
|
||||
appState.traffics = [];
|
||||
appState.totalTraffic = Traffic();
|
||||
appState.runTime = null;
|
||||
}
|
||||
}
|
||||
@@ -100,9 +98,13 @@ class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateProfile(Profile profile) async {
|
||||
await profile.update();
|
||||
config.setProfile(await profile.update());
|
||||
Future<void> updateProfile(String id) async {
|
||||
final profile = config.getCurrentProfileForId(id);
|
||||
if (profile != null) {
|
||||
final tempProfile = profile.copyWith();
|
||||
await tempProfile.update();
|
||||
config.setProfile(tempProfile);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
||||
@@ -145,7 +147,7 @@ class AppController {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
updateProfile(profile);
|
||||
await updateProfile(profile.id);
|
||||
} catch (e) {
|
||||
appState.addLog(
|
||||
Log(
|
||||
@@ -162,7 +164,7 @@ class AppController {
|
||||
if (profile.type == ProfileType.file) {
|
||||
continue;
|
||||
}
|
||||
await updateProfile(profile);
|
||||
await updateProfile(profile.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +268,6 @@ class AppController {
|
||||
}
|
||||
|
||||
init() async {
|
||||
updateLogStatus();
|
||||
if (!config.silentLaunch) {
|
||||
window?.show();
|
||||
}
|
||||
@@ -278,7 +279,7 @@ class AppController {
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
}, title: appLocalizations.init);
|
||||
},title: appLocalizations.init);
|
||||
} else {
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
@@ -290,13 +291,14 @@ class AppController {
|
||||
}
|
||||
|
||||
afterInit() async {
|
||||
await proxyManager.updateStartTime();
|
||||
if (proxyManager.isStart) {
|
||||
if (config.autoRun) {
|
||||
await updateSystemProxy(true);
|
||||
} else {
|
||||
await updateSystemProxy(config.autoRun);
|
||||
await proxyManager.updateStartTime();
|
||||
await updateSystemProxy(proxyManager.isStart);
|
||||
}
|
||||
autoUpdateProfiles();
|
||||
updateLogStatus();
|
||||
autoCheckUpdate();
|
||||
}
|
||||
|
||||
@@ -365,9 +367,11 @@ class AppController {
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
final profile = await commonScaffoldState?.loadingRun<Profile>(
|
||||
() async {
|
||||
return await Profile.normal(
|
||||
final profile = Profile(
|
||||
url: url,
|
||||
).update();
|
||||
);
|
||||
await profile.update();
|
||||
return profile;
|
||||
},
|
||||
title: "${appLocalizations.add}${appLocalizations.profile}",
|
||||
);
|
||||
@@ -390,7 +394,9 @@ class AppController {
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
return await Profile.normal(label: platformFile?.name).saveFile(bytes);
|
||||
final profile = Profile(label: platformFile?.name);
|
||||
await profile.saveFile(bytes);
|
||||
return profile;
|
||||
},
|
||||
title: "${appLocalizations.add}${appLocalizations.profile}",
|
||||
);
|
||||
|
||||
@@ -64,8 +64,3 @@ enum RecoveryOption {
|
||||
all,
|
||||
onlyProfiles,
|
||||
}
|
||||
|
||||
enum ChipType {
|
||||
action,
|
||||
delete,
|
||||
}
|
||||
|
||||
@@ -35,13 +35,6 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
packagesListenable.dispose();
|
||||
}
|
||||
|
||||
Widget _buildAppProxyModePopup() {
|
||||
final items = [
|
||||
CommonPopupMenuItem(
|
||||
|
||||
@@ -228,13 +228,6 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_obscureController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
|
||||
@@ -1,435 +1,140 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
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/plugins/app.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((_) {
|
||||
connectionsNotifier.value = connectionsNotifier.value
|
||||
.copyWith(connections: clashCore.getConnections());
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
timer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(timer) {
|
||||
connectionsNotifier.value = connectionsNotifier.value
|
||||
.copyWith(connections: 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),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
)
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_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) {
|
||||
clashCore.closeConnections(id);
|
||||
connectionsNotifier.value = connectionsNotifier.value
|
||||
.copyWith(connections: clashCore.getConnections());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
timer?.cancel();
|
||||
connectionsNotifier.dispose();
|
||||
_scrollController.dispose();
|
||||
timer = null;
|
||||
}
|
||||
|
||||
@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: 8,
|
||||
spacing: 8,
|
||||
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,
|
||||
onBlock: _handleBlockConnection,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: connections.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionItem extends StatelessWidget {
|
||||
final Connection connection;
|
||||
final Function(String)? onClick;
|
||||
final Function(String)? onBlock;
|
||||
|
||||
const ConnectionItem({
|
||||
super.key,
|
||||
required this.connection,
|
||||
this.onClick,
|
||||
this.onBlock,
|
||||
});
|
||||
|
||||
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) {
|
||||
return ListItem(
|
||||
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||
leading: Platform.isAndroid
|
||||
? 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: 12,
|
||||
),
|
||||
Text(
|
||||
_getSourceText(connection),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final chain in connection.chains)
|
||||
CommonChip(
|
||||
label: chain,
|
||||
onPressed: () {
|
||||
if (onClick == null) return;
|
||||
onClick!(chain);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.block),
|
||||
onPressed: () {
|
||||
if (onBlock == null) return;
|
||||
onBlock!(connection.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
clashCore.closeConnections(id);
|
||||
connectionsNotifier.value = connectionsNotifier.value
|
||||
.copyWith(connections: 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: 8,
|
||||
spacing: 8,
|
||||
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,
|
||||
onBlock: _handleBlockConnection,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: _results.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
// import 'dart:async';
|
||||
//
|
||||
// import 'package:fl_clash/clash/core.dart';
|
||||
// import 'package:fl_clash/models/models.dart';
|
||||
// import 'package:fl_clash/plugins/app.dart';
|
||||
// import 'package:fl_clash/state.dart';
|
||||
// import 'package:fl_clash/widgets/widgets.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
//
|
||||
// class ConnectionsFragment extends StatefulWidget {
|
||||
// const ConnectionsFragment({super.key});
|
||||
//
|
||||
// @override
|
||||
// State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
|
||||
// }
|
||||
//
|
||||
// class _ConnectionsFragmentState extends State<ConnectionsFragment> {
|
||||
// final connectionsNotifier = ValueNotifier<List<Connection>>([]);
|
||||
// Map<String, String?> idPackageNameMap = {};
|
||||
//
|
||||
// Timer? timer;
|
||||
//
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
// _getConnections();
|
||||
// if (timer != null) {
|
||||
// timer?.cancel();
|
||||
// timer = null;
|
||||
// }
|
||||
// timer = Timer.periodic(const Duration(seconds: 3), (timer) {
|
||||
// if (mounted) {
|
||||
// _getConnections();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// _getConnections() {
|
||||
// connectionsNotifier.value = clashCore
|
||||
// .getConnections();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// void dispose() {
|
||||
// super.dispose();
|
||||
// timer?.cancel();
|
||||
// timer = null;
|
||||
// }
|
||||
//
|
||||
// Future<ImageProvider?> _getPackageIconWithConnection(
|
||||
// Connection connection) async {
|
||||
// final uid = connection.metadata.uid;
|
||||
// // if(globalState.packageNameMap[uid] == null){
|
||||
// // globalState.packageNameMap[uid] = await app?.getPackageName(connection.metadata);
|
||||
// // }
|
||||
// final packageName = globalState.packageNameMap[uid];
|
||||
// if(packageName == null) return null;
|
||||
// return await app?.getPackageIcon(packageName);
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return ValueListenableBuilder<List<Connection>>(
|
||||
// valueListenable: connectionsNotifier,
|
||||
// builder: (_, List<Connection> connections, __) {
|
||||
// if (connections.isEmpty) {
|
||||
// return const NullStatus(
|
||||
// label: "未开启代理,或者没有连接数据",
|
||||
// );
|
||||
// }
|
||||
// return ListView.separated(
|
||||
// physics: const AlwaysScrollableScrollPhysics(),
|
||||
// itemBuilder: (_, index) {
|
||||
// final connection = connections[index];
|
||||
// return ListTile(
|
||||
// titleAlignment: ListTileTitleAlignment.top,
|
||||
// leading: Container(
|
||||
// margin: const EdgeInsets.only(top: 4),
|
||||
// width: 48,
|
||||
// height: 48,
|
||||
// child: FutureBuilder<ImageProvider?>(
|
||||
// future: _getPackageIconWithConnection(connection),
|
||||
// builder: (_, snapshot) {
|
||||
// if (!snapshot.hasData && snapshot.data == null) {
|
||||
// return Container();
|
||||
// } else {
|
||||
// return Image(
|
||||
// image: snapshot.data!,
|
||||
// gaplessPlayback: true,
|
||||
// width: 48,
|
||||
// height: 48,
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// contentPadding:
|
||||
// const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
// title: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text(connection.metadata.host.isNotEmpty
|
||||
// ? connection.metadata.host
|
||||
// : connection.metadata.destinationIP),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(
|
||||
// top: 12,
|
||||
// ),
|
||||
// child: Wrap(
|
||||
// runSpacing: 8,
|
||||
// spacing: 8,
|
||||
// children: [
|
||||
// for (final chain in connection.chains)
|
||||
// CommonChip(
|
||||
// label: chain,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// trailing: IconButton(
|
||||
// icon: const Icon(Icons.block),
|
||||
// onPressed: () {},
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// separatorBuilder: (BuildContext context, int index) {
|
||||
// return const Divider(
|
||||
// height: 0,
|
||||
// );
|
||||
// },
|
||||
// itemCount: connections.length,
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -31,7 +31,7 @@ class CoreInfo extends StatelessWidget {
|
||||
style: context
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.toSoftBold,
|
||||
?.toSoftBold(),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -44,7 +44,7 @@ class CoreInfo extends StatelessWidget {
|
||||
style: context
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.toSoftBold,
|
||||
?.toSoftBold(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'network_detection.dart';
|
||||
import 'core_info.dart';
|
||||
import 'outbound_mode.dart';
|
||||
import 'start_button.dart';
|
||||
import 'network_speed.dart';
|
||||
@@ -56,7 +56,7 @@ class _DashboardFragmentState extends State<DashboardFragment> {
|
||||
),
|
||||
GridItem(
|
||||
crossAxisCellCount: isDesktop ? 4 : 6,
|
||||
child: const IntranetIp(),
|
||||
child: const CoreInfo(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IntranetIp extends StatefulWidget {
|
||||
const IntranetIp({super.key});
|
||||
|
||||
@override
|
||||
State<IntranetIp> createState() => _IntranetIpState();
|
||||
}
|
||||
|
||||
class _IntranetIpState extends State<IntranetIp> {
|
||||
final ipNotifier = ValueNotifier<String>("");
|
||||
|
||||
Future<String?> getLocalIpAddress() async {
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list();
|
||||
for (final interface in interfaces) {
|
||||
for (final address in interface.addresses) {
|
||||
if (!address.isLoopback) {
|
||||
return address.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
ipNotifier.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
ipNotifier.value = await getLocalIpAddress() ?? "";
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
info: Info(
|
||||
label: appLocalizations.intranetIp,
|
||||
iconData: Icons.devices,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||
height: globalState.appController.measure.titleLargeHeight + 24 - 1,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: ipNotifier,
|
||||
builder: (_, value, __) {
|
||||
return FadeBox(
|
||||
child: value.isNotEmpty
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
value,
|
||||
style: context.textTheme.titleLarge?.toSoftBold.toMinus,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const Padding(
|
||||
padding: EdgeInsets.all(2),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
final timeoutNotifier = ValueNotifier<bool>(false);
|
||||
bool? _preIsStart;
|
||||
CancelToken? cancelToken;
|
||||
Function? _checkIpDebounce;
|
||||
|
||||
_checkIp(
|
||||
bool isInit,
|
||||
@@ -45,7 +44,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
}
|
||||
|
||||
_checkIpContainer(Widget child) {
|
||||
_checkIpDebounce = debounce(_checkIp);
|
||||
return Selector2<AppState, Config, CheckIpSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
return CheckIpSelectorState(
|
||||
@@ -55,22 +53,13 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
);
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
if (_checkIpDebounce != null) {
|
||||
_checkIpDebounce!([state.isInit, state.isStart]);
|
||||
}
|
||||
_checkIp(state.isInit, state.isStart);
|
||||
return child;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
ipInfoNotifier.dispose();
|
||||
timeoutNotifier.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _checkIpContainer(
|
||||
@@ -135,7 +124,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
),
|
||||
Container(
|
||||
height:
|
||||
globalState.appController.measure.titleLargeHeight + 24 - 1,
|
||||
globalState.appController.measure.titleLargeHeight + 24,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||
child: FadeBox(
|
||||
@@ -150,7 +139,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
text: Text(
|
||||
ipInfo.ip,
|
||||
style: context.textTheme.titleLarge
|
||||
?.toSoftBold.toMinus,
|
||||
?.toSoftBold(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -164,9 +153,9 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
if (timeout) {
|
||||
return Text(
|
||||
"timeout",
|
||||
style: context.textTheme.titleLarge
|
||||
style: context.textTheme.titleMedium
|
||||
?.copyWith(color: Colors.red)
|
||||
.toSoftBold.toMinus,
|
||||
.toSoftBold(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
|
||||
@@ -47,8 +47,8 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
final showValue = value.showValue;
|
||||
final showUnit = "${value.showUnit}/s";
|
||||
final titleLargeSoftBold =
|
||||
Theme.of(context).textTheme.titleLarge?.toSoftBold;
|
||||
final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight;
|
||||
Theme.of(context).textTheme.titleLarge?.toSoftBold();
|
||||
final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight();
|
||||
final valueText = Text(
|
||||
showValue,
|
||||
style: titleLargeSoftBold,
|
||||
@@ -75,7 +75,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.titleSmall?.toSoftBold,
|
||||
style: Theme.of(context).textTheme.titleSmall?.toSoftBold(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -67,7 +67,7 @@ class OutboundMode extends StatelessWidget {
|
||||
.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.toSoftBold,
|
||||
?.toSoftBold(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -13,17 +13,19 @@ class StartButton extends StatefulWidget {
|
||||
|
||||
class _StartButtonState extends State<StartButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
bool isStart = false;
|
||||
bool isInit = false;
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isStart = globalState.appController.appState.isStart;
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
value: 0,
|
||||
value: isStart ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -33,12 +35,9 @@ class _StartButtonState extends State<StartButton>
|
||||
}
|
||||
|
||||
handleSwitchStart() {
|
||||
final appController = globalState.appController;
|
||||
if (isStart == appController.appState.isStart) {
|
||||
isStart = !isStart;
|
||||
updateController();
|
||||
appController.updateSystemProxy(isStart);
|
||||
}
|
||||
isStart = !isStart;
|
||||
updateController();
|
||||
updateSystemProxy();
|
||||
}
|
||||
|
||||
updateController() {
|
||||
@@ -49,18 +48,11 @@ class _StartButtonState extends State<StartButton>
|
||||
}
|
||||
}
|
||||
|
||||
Widget _updateControllerContainer(Widget child) {
|
||||
return Selector<AppState, bool>(
|
||||
selector: (_, appState) => appState.isStart,
|
||||
builder: (_, isStart, child) {
|
||||
if(isStart != this.isStart){
|
||||
this.isStart = isStart;
|
||||
updateController();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
updateSystemProxy() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final appController = globalState.appController;
|
||||
await appController.updateSystemProxy(isStart);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -80,7 +72,8 @@ class _StartButtonState extends State<StartButton>
|
||||
other.getTimeDifference(
|
||||
DateTime.now(),
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium?.toSoftBold(),
|
||||
),
|
||||
)
|
||||
.width +
|
||||
@@ -126,14 +119,24 @@ class _StartButtonState extends State<StartButton>
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _updateControllerContainer(
|
||||
Selector<AppState, int?>(
|
||||
child: Selector<AppState, bool>(
|
||||
selector: (_, appState) => appState.runTime != null,
|
||||
builder: (_, isRun, child) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isStart != isRun) {
|
||||
isStart = isRun;
|
||||
updateController();
|
||||
}
|
||||
});
|
||||
return child!;
|
||||
},
|
||||
child: Selector<AppState, int?>(
|
||||
selector: (_, appState) => appState.runTime,
|
||||
builder: (_, int? value, __) {
|
||||
final text = other.getTimeText(value);
|
||||
return Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -42,7 +42,7 @@ class TrafficUsage extends StatelessWidget {
|
||||
),
|
||||
Text(
|
||||
trafficValue.showUnit,
|
||||
style: context.textTheme.labelMedium?.toLight,
|
||||
style: context.textTheme.labelMedium?.toLight(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export 'proxies/proxies.dart';
|
||||
export 'proxies.dart';
|
||||
export 'dashboard/dashboard.dart';
|
||||
export 'tools.dart';
|
||||
export 'profiles/profiles.dart';
|
||||
|
||||
@@ -17,12 +17,7 @@ class LogsFragment extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LogsFragmentState extends State<LogsFragment> {
|
||||
final logsNotifier = ValueNotifier<LogsAndKeywords>(const LogsAndKeywords());
|
||||
final scrollController = ScrollController(
|
||||
keepScrollOffset: false,
|
||||
);
|
||||
List<GlobalObjectKey<_LogItemState>> keys = [];
|
||||
|
||||
final logsNotifier = ValueNotifier<List<Log>>([]);
|
||||
Timer? timer;
|
||||
|
||||
@override
|
||||
@@ -30,18 +25,18 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appState = globalState.appController.appState;
|
||||
logsNotifier.value = logsNotifier.value.copyWith(logs: appState.logs);
|
||||
logsNotifier.value = List<Log>.from(appState.logs);
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||
final logs = appState.logs;
|
||||
final logs = List<Log>.from(appState.logs);
|
||||
if (!const ListEquality<Log>().equals(
|
||||
logsNotifier.value.logs,
|
||||
logsNotifier.value,
|
||||
logs,
|
||||
)) {
|
||||
logsNotifier.value = logsNotifier.value.copyWith(logs: logs);
|
||||
logsNotifier.value = logs;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -51,8 +46,6 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
timer?.cancel();
|
||||
logsNotifier.dispose();
|
||||
scrollController.dispose();
|
||||
timer = null;
|
||||
}
|
||||
|
||||
@@ -66,7 +59,7 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: LogsSearchDelegate(
|
||||
logs: logsNotifier.value,
|
||||
logs: logsNotifier.value.reversed.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -79,23 +72,32 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
});
|
||||
}
|
||||
|
||||
_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,
|
||||
_buildList() {
|
||||
return ValueListenableBuilder<List<Log>>(
|
||||
valueListenable: logsNotifier,
|
||||
builder: (_, List<Log> logs, __) {
|
||||
if (logs.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullLogsDesc,
|
||||
);
|
||||
}
|
||||
logs = logs.reversed.toList();
|
||||
return ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: logs.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final log = logs[index];
|
||||
return LogItem(
|
||||
log: log,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,88 +114,21 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: ValueListenableBuilder<LogsAndKeywords>(
|
||||
valueListenable: logsNotifier,
|
||||
builder: (_, state, __) {
|
||||
var logs = state.filteredLogs;
|
||||
if (logs.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullLogsDesc,
|
||||
);
|
||||
}
|
||||
logs = logs.reversed.toList();
|
||||
keys = logs
|
||||
.map((log) => GlobalObjectKey<_LogItemState>(log.dateTime))
|
||||
.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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
controller: scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
final log = logs[index];
|
||||
return LogItem(
|
||||
key: Key(log.dateTime.toString()),
|
||||
log: log,
|
||||
onClick: _addKeyword,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: logs.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
child: _buildList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LogsSearchDelegate extends SearchDelegate {
|
||||
ValueNotifier<LogsAndKeywords> logsNotifier;
|
||||
List<Log> logs = [];
|
||||
|
||||
LogsSearchDelegate({
|
||||
required LogsAndKeywords logs,
|
||||
}) : logsNotifier = ValueNotifier(logs);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
logsNotifier.dispose();
|
||||
}
|
||||
|
||||
get state => logsNotifier.value;
|
||||
required this.logs,
|
||||
});
|
||||
|
||||
List<Log> get _results {
|
||||
final lowQuery = query.toLowerCase();
|
||||
return logsNotifier.value.filteredLogs
|
||||
return logs
|
||||
.where(
|
||||
(log) =>
|
||||
(log.payload?.toLowerCase().contains(lowQuery) ?? false) ||
|
||||
@@ -236,98 +171,37 @@ class LogsSearchDelegate extends SearchDelegate {
|
||||
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: 8,
|
||||
spacing: 8,
|
||||
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);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: _results.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
return ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: _results.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final log = _results[index];
|
||||
return LogItem(
|
||||
key: ValueKey(log.dateTime),
|
||||
log: log,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LogItem extends StatefulWidget {
|
||||
class LogItem extends StatelessWidget {
|
||||
final Log log;
|
||||
final Function(String)? onClick;
|
||||
|
||||
const LogItem({
|
||||
super.key,
|
||||
required this.log,
|
||||
this.onClick,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LogItem> createState() => _LogItemState();
|
||||
}
|
||||
|
||||
class _LogItemState extends State<LogItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final log = widget.log;
|
||||
return ListTile(
|
||||
title: SelectableText(log.payload ?? ''),
|
||||
subtitle: Column(
|
||||
@@ -349,10 +223,6 @@ class _LogItemState extends State<LogItem> {
|
||||
vertical: 8,
|
||||
),
|
||||
child: CommonChip(
|
||||
onPressed: () {
|
||||
if (widget.onClick == null) return;
|
||||
widget.onClick!(log.logLevel.name);
|
||||
},
|
||||
label: log.logLevel.name,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -91,7 +91,6 @@ class _URLFormDialogState extends State<URLFormDialog> {
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
TextField(
|
||||
maxLines: null,
|
||||
controller: urlController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
|
||||
@@ -41,26 +41,19 @@ class _EditProfileState extends State<EditProfile> {
|
||||
_handleConfirm() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
final config = widget.context.read<Config>();
|
||||
final profile = widget.profile.copyWith(
|
||||
url: urlController.text,
|
||||
label: labelController.text,
|
||||
autoUpdate: autoUpdate,
|
||||
autoUpdateDuration: Duration(
|
||||
minutes: int.parse(
|
||||
autoUpdateDurationController.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
final hasUpdate = widget.profile.url != profile.url;
|
||||
config.setProfile(profile);
|
||||
final hasUpdate = urlController.text.isNotEmpty && widget.profile.url != urlController.text;
|
||||
widget.profile.url = urlController.text;
|
||||
widget.profile.label = labelController.text;
|
||||
widget.profile.autoUpdate = autoUpdate;
|
||||
widget.profile.autoUpdateDuration =
|
||||
Duration(minutes: int.parse(autoUpdateDurationController.text));
|
||||
config.setProfile(widget.profile);
|
||||
if (hasUpdate) {
|
||||
globalState.homeScaffoldKey.currentState?.loadingRun(
|
||||
() async {
|
||||
if (hasUpdate) {
|
||||
await globalState.appController.updateProfile(profile);
|
||||
}
|
||||
},
|
||||
);
|
||||
widget.context.findAncestorStateOfType<CommonScaffoldState>()?.loadingRun(
|
||||
() => globalState.appController.updateProfile(
|
||||
widget.profile.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@@ -90,11 +83,12 @@ class _EditProfileState extends State<EditProfile> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.profile.type == ProfileType.url) ...[
|
||||
if (widget.profile.type == ProfileType.url)...[
|
||||
ListItem(
|
||||
title: TextFormField(
|
||||
controller: urlController,
|
||||
maxLines: null,
|
||||
minLines: 1,
|
||||
maxLines: 2,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: appLocalizations.url,
|
||||
|
||||
@@ -29,8 +29,6 @@ class ProfilesFragment extends StatefulWidget {
|
||||
class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
final hasPadding = ValueNotifier<bool>(false);
|
||||
|
||||
List<GlobalObjectKey<_ProfileItemState>> profileItemKeys = [];
|
||||
|
||||
_handleShowAddExtendPage() {
|
||||
showExtendPage(
|
||||
globalState.navigatorKey.currentState!.context,
|
||||
@@ -52,22 +50,19 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
}
|
||||
}
|
||||
|
||||
_updateProfiles() async {
|
||||
final updateProfiles = profileItemKeys.map<Future>(
|
||||
(key) async => await key.currentState?.updateProfile(false));
|
||||
final result = await Future.wait(updateProfiles);
|
||||
}
|
||||
|
||||
_initScaffoldState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
if (!context.mounted) return;
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_updateProfiles();
|
||||
commonScaffoldState.loadingRun<void>(
|
||||
() async {
|
||||
await globalState.appController.updateProfiles();
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.sync),
|
||||
),
|
||||
@@ -86,12 +81,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
hasPadding.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<AppState, bool>(
|
||||
@@ -114,9 +103,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
label: appLocalizations.nullProfileDesc,
|
||||
);
|
||||
}
|
||||
profileItemKeys = state.profiles
|
||||
.map((profile) => GlobalObjectKey<_ProfileItemState>(profile.id))
|
||||
.toList();
|
||||
final columns = _getColumns(state.viewMode);
|
||||
final isMobile = state.viewMode == ViewMode.mobile;
|
||||
return Align(
|
||||
@@ -148,11 +134,10 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
crossAxisSpacing: 16,
|
||||
crossAxisCount: columns,
|
||||
children: [
|
||||
for (int i = 0; i < state.profiles.length; i++)
|
||||
for (final profile in state.profiles)
|
||||
GridItem(
|
||||
child: ProfileItem(
|
||||
key: profileItemKeys[i],
|
||||
profile: state.profiles[i],
|
||||
profile: profile,
|
||||
groupValue: state.currentProfileId,
|
||||
onChanged:
|
||||
globalState.appController.changeProfile,
|
||||
@@ -190,44 +175,36 @@ class ProfileItem extends StatefulWidget {
|
||||
class _ProfileItemState extends State<ProfileItem> {
|
||||
final isUpdating = ValueNotifier<bool>(false);
|
||||
|
||||
_handleDeleteProfile() async {
|
||||
globalState.appController.deleteProfile(widget.profile.id);
|
||||
_handleDeleteProfile(String id) async {
|
||||
globalState.appController.deleteProfile(id);
|
||||
}
|
||||
|
||||
_handleUpdateProfile() async {
|
||||
await globalState.safeRun<void>(updateProfile);
|
||||
}
|
||||
|
||||
Future updateProfile([isSingle = true]) async {
|
||||
_handleUpdateProfile(String id) async {
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
await globalState.appController.updateProfile(widget.profile);
|
||||
} catch (e) {
|
||||
isUpdating.value = false;
|
||||
if (!isSingle) {
|
||||
return e.toString();
|
||||
}
|
||||
}
|
||||
await globalState.safeRun<void>(() async {
|
||||
await globalState.appController.updateProfile(id);
|
||||
});
|
||||
isUpdating.value = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
_handleShowEditExtendPage() {
|
||||
_handleShowEditExtendPage(
|
||||
Profile profile,
|
||||
) {
|
||||
showExtendPage(
|
||||
context,
|
||||
body: EditProfile(
|
||||
profile: widget.profile,
|
||||
profile: profile.copyWith(),
|
||||
context: context,
|
||||
),
|
||||
title: "${appLocalizations.edit}${appLocalizations.profile}",
|
||||
);
|
||||
}
|
||||
|
||||
_handleViewProfile() {
|
||||
_handleViewProfile(Profile profile) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ViewProfile(
|
||||
profile: widget.profile,
|
||||
profile: profile.copyWith(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -235,6 +212,15 @@ class _ProfileItemState extends State<ProfileItem> {
|
||||
|
||||
_buildTitle(Profile profile) {
|
||||
final textTheme = context.textTheme;
|
||||
final userInfo = profile.userInfo ?? UserInfo();
|
||||
final use = userInfo.upload + userInfo.download;
|
||||
final total = userInfo.total;
|
||||
final useShow = TrafficValue(value: use).show;
|
||||
final totalShow = TrafficValue(value: total).show;
|
||||
final progress = total == 0 ? 0.0 : use / total;
|
||||
final expireShow = userInfo.expire == 0
|
||||
? appLocalizations.infiniteTime
|
||||
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000).show;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
@@ -253,141 +239,53 @@ class _ProfileItemState extends State<ProfileItem> {
|
||||
),
|
||||
Text(
|
||||
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',
|
||||
style: textTheme.labelMedium?.toLight,
|
||||
style: textTheme.labelMedium?.toLight(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final userInfo = profile.userInfo ?? const UserInfo();
|
||||
final use = userInfo.upload + userInfo.download;
|
||||
final total = userInfo.total;
|
||||
final useShow = TrafficValue(value: use).show;
|
||||
final totalShow = TrafficValue(value: total).show;
|
||||
final progress = total == 0 ? 0.0 : use / total;
|
||||
final expireShow = userInfo.expire == 0
|
||||
? appLocalizations.infiniteTime
|
||||
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000)
|
||||
.show;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 6,
|
||||
value: progress,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"$useShow / $totalShow",
|
||||
style: textTheme.labelMedium?.toLight(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
appLocalizations.expirationTime,
|
||||
style: textTheme.labelMedium?.toLighter(),
|
||||
),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 6,
|
||||
value: progress,
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"$useShow / $totalShow",
|
||||
style: textTheme.labelMedium?.toLight,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
appLocalizations.expirationTime,
|
||||
style: textTheme.labelMedium?.toLighter,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
expireShow,
|
||||
style: textTheme.labelMedium?.toLighter,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
// final child = switch (userInfo != null) {
|
||||
// true => () {
|
||||
// final use = userInfo!.upload + userInfo.download;
|
||||
// final total = userInfo.total;
|
||||
// final useShow = TrafficValue(value: use).show;
|
||||
// final totalShow = TrafficValue(value: total).show;
|
||||
// final progress = total == 0 ? 0.0 : use / total;
|
||||
// final expireShow = userInfo.expire == 0
|
||||
// ? appLocalizations.infiniteTime
|
||||
// : DateTime.fromMillisecondsSinceEpoch(
|
||||
// userInfo.expire * 1000)
|
||||
// .show;
|
||||
// return Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Container(
|
||||
// margin: const EdgeInsets.symmetric(
|
||||
// vertical: 8,
|
||||
// ),
|
||||
// child: LinearProgressIndicator(
|
||||
// minHeight: 6,
|
||||
// value: progress,
|
||||
// ),
|
||||
// ),
|
||||
// Text(
|
||||
// "$useShow / $totalShow",
|
||||
// style: textTheme.labelMedium?.toLight(),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 2,
|
||||
// ),
|
||||
// Row(
|
||||
// children: [
|
||||
// Text(
|
||||
// appLocalizations.expirationTime,
|
||||
// style: textTheme.labelMedium?.toLighter(),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// width: 4,
|
||||
// ),
|
||||
// Text(
|
||||
// expireShow,
|
||||
// style: textTheme.labelMedium?.toLighter(),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
// ],
|
||||
// );
|
||||
// }(),
|
||||
// false => Column(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(top: 8),
|
||||
// child: CommonChip(
|
||||
// onPressed: _handleViewProfile,
|
||||
// avatar: const Icon(Icons.remove_red_eye),
|
||||
// label: appLocalizations.view,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// };
|
||||
// final measure = globalState.appController.measure;
|
||||
// final height = 6 + 8 * 2 + 2 + measure.labelMediumHeight * 2;
|
||||
// return SizedBox(
|
||||
// height: height,
|
||||
// child: child,
|
||||
// );
|
||||
}),
|
||||
Text(
|
||||
expireShow,
|
||||
style: textTheme.labelMedium?.toLighter(),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isUpdating.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profile = widget.profile;
|
||||
@@ -412,63 +310,48 @@ class _ProfileItemState extends State<ProfileItem> {
|
||||
onChanged: onChanged,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
trailing: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: isUpdating,
|
||||
builder: (_, isUpdating, ___) {
|
||||
return FadeBox(
|
||||
child: isUpdating
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: CommonPopupMenu<ProfileActions>(
|
||||
items: [
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.edit,
|
||||
label: appLocalizations.edit,
|
||||
iconData: Icons.edit,
|
||||
),
|
||||
if (profile.type == ProfileType.url)
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.update,
|
||||
label: appLocalizations.update,
|
||||
iconData: Icons.sync,
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.delete,
|
||||
label: appLocalizations.delete,
|
||||
iconData: Icons.delete,
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.view,
|
||||
label: "查看",
|
||||
iconData: Icons.visibility,
|
||||
),
|
||||
],
|
||||
onSelected: (ProfileActions? action) async {
|
||||
switch (action) {
|
||||
case ProfileActions.edit:
|
||||
_handleShowEditExtendPage();
|
||||
break;
|
||||
case ProfileActions.delete:
|
||||
_handleDeleteProfile();
|
||||
break;
|
||||
case ProfileActions.update:
|
||||
_handleUpdateProfile();
|
||||
break;
|
||||
case ProfileActions.view:
|
||||
_handleViewProfile();
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
trailing: CommonPopupMenu<ProfileActions>(
|
||||
items: [
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.edit,
|
||||
label: appLocalizations.edit,
|
||||
iconData: Icons.edit,
|
||||
),
|
||||
if (profile.type == ProfileType.url)
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.update,
|
||||
label: appLocalizations.update,
|
||||
iconData: Icons.sync,
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.delete,
|
||||
label: appLocalizations.delete,
|
||||
iconData: Icons.delete,
|
||||
),
|
||||
// CommonPopupMenuItem(
|
||||
// action: ProfileActions.view,
|
||||
// label: "查看",
|
||||
// iconData: Icons.visibility,
|
||||
// ),
|
||||
],
|
||||
onSelected: (ProfileActions? action) async {
|
||||
switch (action) {
|
||||
case ProfileActions.edit:
|
||||
_handleShowEditExtendPage(profile);
|
||||
break;
|
||||
case ProfileActions.delete:
|
||||
_handleDeleteProfile(profile.id);
|
||||
break;
|
||||
case ProfileActions.update:
|
||||
_handleUpdateProfile(profile.id);
|
||||
break;
|
||||
case ProfileActions.view:
|
||||
_handleViewProfile(profile);
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
title: _buildTitle(profile),
|
||||
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:re_editor/re_editor.dart';
|
||||
@@ -23,9 +22,7 @@ class ViewProfile extends StatefulWidget {
|
||||
|
||||
class _ViewProfileState extends State<ViewProfile> {
|
||||
bool readOnly = true;
|
||||
CodeLineEditingController? controller;
|
||||
final contentNotifier = ValueNotifier<String>("");
|
||||
final key = GlobalKey<CommonScaffoldState>();
|
||||
final controller = CodeLineEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -37,120 +34,80 @@ class _ViewProfileState extends State<ViewProfile> {
|
||||
}
|
||||
final file = File(profilePath);
|
||||
final text = await file.readAsString();
|
||||
contentNotifier.value = text;
|
||||
controller.text = text;
|
||||
// _codeController.text = text;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
contentNotifier.dispose();
|
||||
controller?.dispose();
|
||||
}
|
||||
|
||||
Profile get profile => widget.profile;
|
||||
|
||||
_handleChangeReadOnly() async {
|
||||
if (readOnly == true) {
|
||||
setState(() {
|
||||
readOnly = false;
|
||||
});
|
||||
} else {
|
||||
final text = controller?.text;
|
||||
if (text == null || text == contentNotifier.value) {
|
||||
setState(() {
|
||||
readOnly = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
contentNotifier.value = text;
|
||||
final newProfile = await key.currentState?.loadingRun<Profile>(() async {
|
||||
return await profile.saveFileWithString(text);
|
||||
});
|
||||
if (newProfile == null) return;
|
||||
globalState.appController.config.setProfile(newProfile);
|
||||
setState(() {
|
||||
readOnly = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonScaffold(
|
||||
key: key,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: controller?.undo,
|
||||
onPressed: controller.undo,
|
||||
icon: const Icon(Icons.undo),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: controller?.redo,
|
||||
onPressed: controller.redo,
|
||||
icon: const Icon(Icons.redo),
|
||||
),
|
||||
if (!widget.profile.realAutoUpdate)
|
||||
IconButton(
|
||||
onPressed: _handleChangeReadOnly,
|
||||
icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
)
|
||||
],
|
||||
body: ValueListenableBuilder(
|
||||
valueListenable: contentNotifier,
|
||||
builder: (_, value, __) {
|
||||
if (value.isEmpty) return Container();
|
||||
controller = CodeLineEditingController.fromText(value);
|
||||
return CodeEditor(
|
||||
autofocus: false,
|
||||
readOnly: readOnly,
|
||||
scrollbarBuilder: (context, child, details) {
|
||||
return Scrollbar(
|
||||
controller: details.controller,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(2),
|
||||
interactive: true,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
showCursorWhenReadOnly: false,
|
||||
controller: controller,
|
||||
toolbarController:
|
||||
!readOnly ? const ContextMenuControllerImpl() : null,
|
||||
shortcutsActivatorsBuilder:
|
||||
const DefaultCodeShortcutsActivatorsBuilder(),
|
||||
indicatorBuilder:
|
||||
(context, editingController, chunkController, notifier) {
|
||||
return Row(
|
||||
children: [
|
||||
DefaultCodeLineNumber(
|
||||
controller: editingController,
|
||||
notifier: notifier,
|
||||
),
|
||||
DefaultCodeChunkIndicator(
|
||||
width: 20,
|
||||
controller: chunkController,
|
||||
notifier: notifier,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
style: CodeEditorStyle(
|
||||
fontSize: 14,
|
||||
codeTheme: CodeHighlightTheme(
|
||||
languages: {
|
||||
'yaml': CodeHighlightThemeMode(
|
||||
mode: langYaml,
|
||||
)
|
||||
},
|
||||
theme: intellijLightTheme,
|
||||
),
|
||||
),
|
||||
body: CodeEditor(
|
||||
autofocus: false,
|
||||
readOnly: readOnly,
|
||||
scrollbarBuilder: (context, child, details) {
|
||||
return Scrollbar(
|
||||
controller: details.controller,
|
||||
thumbVisibility: true,
|
||||
interactive: true,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
showCursorWhenReadOnly: false,
|
||||
controller: controller,
|
||||
toolbarController: const ContextMenuControllerImpl(),
|
||||
shortcutsActivatorsBuilder:
|
||||
const DefaultCodeShortcutsActivatorsBuilder(),
|
||||
indicatorBuilder:
|
||||
(context, editingController, chunkController, notifier) {
|
||||
return Row(
|
||||
children: [
|
||||
DefaultCodeLineNumber(
|
||||
controller: editingController,
|
||||
notifier: notifier,
|
||||
),
|
||||
DefaultCodeChunkIndicator(
|
||||
width: 20,
|
||||
controller: chunkController,
|
||||
notifier: notifier,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
style: CodeEditorStyle(
|
||||
fontSize: 14,
|
||||
codeTheme: CodeHighlightTheme(
|
||||
languages: {
|
||||
'yaml': CodeHighlightThemeMode(
|
||||
mode: langYaml,
|
||||
)
|
||||
},
|
||||
theme: intellijLightTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: "查看",
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
readOnly = !readOnly;
|
||||
});
|
||||
},
|
||||
child: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
|
||||
),
|
||||
title: widget.profile.label ?? widget.profile.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -183,6 +140,7 @@ class ContextMenuControllerImpl implements SelectionToolbarController {
|
||||
}
|
||||
showMenu(
|
||||
context: context,
|
||||
popUpAnimationStyle: AnimationStyle.noAnimation,
|
||||
position: RelativeRect.fromSize(
|
||||
(anchors.secondaryAnchor ?? anchors.primaryAnchor) &
|
||||
const Size(150, double.infinity),
|
||||
@@ -190,16 +148,22 @@ class ContextMenuControllerImpl implements SelectionToolbarController {
|
||||
),
|
||||
items: [
|
||||
ContextMenuItemWidget(
|
||||
text: appLocalizations.cut,
|
||||
onTap: controller.cut,
|
||||
text: 'Cut',
|
||||
onTap: () {
|
||||
controller.cut();
|
||||
},
|
||||
),
|
||||
ContextMenuItemWidget(
|
||||
text: appLocalizations.copy,
|
||||
onTap: controller.copy,
|
||||
text: 'Copy',
|
||||
onTap: () {
|
||||
controller.copy();
|
||||
},
|
||||
),
|
||||
ContextMenuItemWidget(
|
||||
text: appLocalizations.paste,
|
||||
onTap: controller.paste,
|
||||
text: 'Paste',
|
||||
onTap: () {
|
||||
controller.paste();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,23 +1,65 @@
|
||||
import 'package:collection/collection.dart';
|
||||
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/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ProxiesTabFragment extends StatefulWidget {
|
||||
const ProxiesTabFragment({super.key});
|
||||
import '../enum/enum.dart';
|
||||
import '../models/models.dart';
|
||||
import '../common/common.dart';
|
||||
import '../widgets/widgets.dart';
|
||||
|
||||
class ProxiesFragment extends StatefulWidget {
|
||||
const ProxiesFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ProxiesTabFragment> createState() => _ProxiesTabFragmentState();
|
||||
State<ProxiesFragment> createState() => _ProxiesFragmentState();
|
||||
}
|
||||
|
||||
class _ProxiesTabFragmentState extends State<ProxiesTabFragment> with TickerProviderStateMixin {
|
||||
class _ProxiesFragmentState extends State<ProxiesFragment>
|
||||
with TickerProviderStateMixin {
|
||||
TabController? _tabController;
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
final items = [
|
||||
CommonPopupMenuItem(
|
||||
action: ProxiesSortType.none,
|
||||
label: appLocalizations.defaultSort,
|
||||
iconData: Icons.sort,
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
action: ProxiesSortType.delay,
|
||||
label: appLocalizations.delaySort,
|
||||
iconData: Icons.network_ping),
|
||||
CommonPopupMenuItem(
|
||||
action: ProxiesSortType.name,
|
||||
label: appLocalizations.nameSort,
|
||||
iconData: Icons.sort_by_alpha),
|
||||
];
|
||||
commonScaffoldState?.actions = [
|
||||
Selector<Config, ProxiesSortType>(
|
||||
selector: (_, config) => config.proxiesSortType,
|
||||
builder: (_, proxiesSortType, __) {
|
||||
return CommonPopupMenu<ProxiesSortType>.radio(
|
||||
items: items,
|
||||
onSelected: (value) {
|
||||
final config = context.read<Config>();
|
||||
config.proxiesSortType = value;
|
||||
},
|
||||
selectedValue: proxiesSortType,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
_handleTabControllerChange() {
|
||||
final indexIsChanging = _tabController?.indexIsChanging ?? false;
|
||||
if (indexIsChanging) return;
|
||||
@@ -29,7 +71,6 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment> with TickerProv
|
||||
appController.config.updateCurrentGroupName(currentGroups[index].name);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
@@ -38,70 +79,79 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment> with TickerProv
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector2<AppState, Config, ProxiesSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
final currentGroups = appState.currentGroups;
|
||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
||||
return ProxiesSelectorState(
|
||||
groupNames: groupNames,
|
||||
currentGroupName: config.currentGroupName,
|
||||
);
|
||||
},
|
||||
shouldRebuild: (prev, next) {
|
||||
if (!const ListEquality<String>()
|
||||
.equals(prev.groupNames, next.groupNames)) {
|
||||
_tabController?.removeListener(_handleTabControllerChange);
|
||||
_tabController?.dispose();
|
||||
_tabController = null;
|
||||
return true;
|
||||
return Selector<AppState, bool>(
|
||||
selector: (_, appState) => appState.currentLabel == 'proxies',
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return false;
|
||||
return child!;
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
final index = state.groupNames.indexWhere(
|
||||
(item) => item == state.currentGroupName,
|
||||
);
|
||||
_tabController ??= TabController(
|
||||
length: state.groupNames.length,
|
||||
initialIndex: index == -1 ? 0 : index,
|
||||
vsync: this,
|
||||
)..addListener(_handleTabControllerChange);
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
dividerColor: Colors.transparent,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
overlayColor:
|
||||
const WidgetStatePropertyAll(Colors.transparent),
|
||||
tabs: [
|
||||
for (final groupName in state.groupNames)
|
||||
Tab(
|
||||
text: groupName,
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
child: Selector2<AppState, Config, ProxiesSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
final currentGroups = appState.currentGroups;
|
||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
||||
return ProxiesSelectorState(
|
||||
groupNames: groupNames,
|
||||
currentGroupName: config.currentGroupName,
|
||||
);
|
||||
},
|
||||
shouldRebuild: (prev, next) {
|
||||
if (!const ListEquality<String>()
|
||||
.equals(prev.groupNames, next.groupNames)) {
|
||||
_tabController?.removeListener(_handleTabControllerChange);
|
||||
_tabController?.dispose();
|
||||
_tabController = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
final index = state.groupNames.indexWhere(
|
||||
(item) => item == state.currentGroupName,
|
||||
);
|
||||
_tabController ??= TabController(
|
||||
length: state.groupNames.length,
|
||||
initialIndex: index == -1 ? 0 : index,
|
||||
vsync: this,
|
||||
)..addListener(_handleTabControllerChange);
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
dividerColor: Colors.transparent,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
overlayColor:
|
||||
const WidgetStatePropertyAll(Colors.transparent),
|
||||
tabs: [
|
||||
for (final groupName in state.groupNames)
|
||||
KeepContainer(
|
||||
key: ObjectKey(groupName),
|
||||
child: ProxiesTabView(
|
||||
groupName: groupName,
|
||||
),
|
||||
Tab(
|
||||
text: groupName,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
for (final groupName in state.groupNames)
|
||||
KeepContainer(
|
||||
key: ObjectKey(groupName),
|
||||
child: ProxiesTabView(
|
||||
groupName: groupName,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -117,7 +167,7 @@ class ProxiesTabView extends StatelessWidget {
|
||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
(a, b) => other.sortByChar(a.name, b.name),
|
||||
(a, b) => other.sortByChar(a.name, b.name),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,7 +175,7 @@ class ProxiesTabView extends StatelessWidget {
|
||||
final appState = context.read<AppState>();
|
||||
return proxies = List.of(proxies)
|
||||
..sort(
|
||||
(a, b) {
|
||||
(a, b) {
|
||||
final aDelay = appState.getDelay(a.name);
|
||||
final bDelay = appState.getDelay(b.name);
|
||||
if (aDelay == null && bDelay == null) {
|
||||
@@ -143,10 +193,10 @@ class ProxiesTabView extends StatelessWidget {
|
||||
}
|
||||
|
||||
_getProxies(
|
||||
BuildContext context,
|
||||
List<Proxy> proxies,
|
||||
ProxiesSortType proxiesSortType,
|
||||
) {
|
||||
BuildContext context,
|
||||
List<Proxy> proxies,
|
||||
ProxiesSortType proxiesSortType,
|
||||
) {
|
||||
if (proxiesSortType == ProxiesSortType.delay) {
|
||||
return _sortOfDelay(context, proxies);
|
||||
}
|
||||
@@ -329,7 +379,7 @@ class ProxyCard extends StatelessWidget {
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color:
|
||||
context.textTheme.bodySmall?.color?.toLight(),
|
||||
context.textTheme.bodySmall?.color?.toLight(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1,59 +0,0 @@
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/widgets/card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ProxiesExpansionPanelFragment extends StatefulWidget {
|
||||
const ProxiesExpansionPanelFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ProxiesExpansionPanelFragment> createState() =>
|
||||
_ProxiesExpansionPanelFragmentState();
|
||||
}
|
||||
|
||||
class _ProxiesExpansionPanelFragmentState
|
||||
extends State<ProxiesExpansionPanelFragment> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector2<AppState, Config, ProxiesSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
final currentGroups = appState.currentGroups;
|
||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
||||
return ProxiesSelectorState(
|
||||
groupNames: groupNames,
|
||||
currentGroupName: config.currentGroupName,
|
||||
);
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: state.groupNames.length,
|
||||
itemBuilder: (_, index) {
|
||||
final groupName = state.groupNames[index];
|
||||
return CommonCard(
|
||||
child: ExpansionTile(
|
||||
title: Text(groupName),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(0.0),
|
||||
side: const BorderSide(color: Colors.transparent),
|
||||
),
|
||||
collapsedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(0.0),
|
||||
side: const BorderSide(color: Colors.transparent),
|
||||
),
|
||||
children: [
|
||||
Text("1212313"),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const SizedBox(
|
||||
height: 16,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/fragments/proxies/tabview.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 ProxiesFragment extends StatefulWidget {
|
||||
const ProxiesFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ProxiesFragment> createState() => _ProxiesFragmentState();
|
||||
}
|
||||
|
||||
class _ProxiesFragmentState extends State<ProxiesFragment> {
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
final items = [
|
||||
CommonPopupMenuItem(
|
||||
action: ProxiesSortType.none,
|
||||
label: appLocalizations.defaultSort,
|
||||
iconData: Icons.sort,
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
action: ProxiesSortType.delay,
|
||||
label: appLocalizations.delaySort,
|
||||
iconData: Icons.network_ping),
|
||||
CommonPopupMenuItem(
|
||||
action: ProxiesSortType.name,
|
||||
label: appLocalizations.nameSort,
|
||||
iconData: Icons.sort_by_alpha),
|
||||
];
|
||||
commonScaffoldState?.actions = [
|
||||
Selector<Config, ProxiesSortType>(
|
||||
selector: (_, config) => config.proxiesSortType,
|
||||
builder: (_, proxiesSortType, __) {
|
||||
return CommonPopupMenu<ProxiesSortType>.radio(
|
||||
items: items,
|
||||
onSelected: (value) {
|
||||
final config = context.read<Config>();
|
||||
config.proxiesSortType = value;
|
||||
},
|
||||
selectedValue: proxiesSortType,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<AppState, bool>(
|
||||
selector: (_, appState) => appState.currentLabel == 'proxies',
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: const ProxiesTabFragment(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,21 @@ import 'dart:io';
|
||||
|
||||
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/plugins/app.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});
|
||||
class RequestFragment extends StatefulWidget {
|
||||
const RequestFragment({super.key});
|
||||
|
||||
@override
|
||||
State<RequestsFragment> createState() => _RequestsFragmentState();
|
||||
State<RequestFragment> createState() => _RequestFragmentState();
|
||||
}
|
||||
|
||||
class _RequestsFragmentState extends State<RequestsFragment> {
|
||||
final requestsNotifier =
|
||||
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
|
||||
class _RequestFragmentState extends State<RequestFragment> {
|
||||
final requestsNotifier = ValueNotifier<List<Connection>>([]);
|
||||
final ScrollController _scrollController = ScrollController(
|
||||
keepScrollOffset: false,
|
||||
);
|
||||
@@ -32,70 +29,23 @@ class _RequestsFragmentState extends State<RequestsFragment> {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appState = globalState.appController.appState;
|
||||
requestsNotifier.value =
|
||||
requestsNotifier.value.copyWith(connections: appState.requests);
|
||||
requestsNotifier.value = List<Connection>.from(appState.requests);
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||
final requests = appState.requests;
|
||||
final requests = List<Connection>.from(appState.requests);
|
||||
if (!const ListEquality<Connection>().equals(
|
||||
requestsNotifier.value.connections,
|
||||
requestsNotifier.value,
|
||||
requests,
|
||||
)) {
|
||||
requestsNotifier.value =
|
||||
requestsNotifier.value.copyWith(connections: requests);
|
||||
requestsNotifier.value = 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),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
)
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_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() {
|
||||
super.dispose();
|
||||
@@ -106,86 +56,42 @@ class _RequestsFragmentState extends State<RequestsFragment> {
|
||||
|
||||
@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: 8,
|
||||
spacing: 8,
|
||||
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 RequestItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: _addKeyword,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: connections.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
return ValueListenableBuilder<List<Connection>>(
|
||||
valueListenable: requestsNotifier,
|
||||
builder: (_, List<Connection> connections, __) {
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullRequestsDesc,
|
||||
);
|
||||
},
|
||||
),
|
||||
}
|
||||
connections = connections.reversed.toList();
|
||||
return ListView.separated(
|
||||
controller: _scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
final connection = connections[index];
|
||||
return RequestItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: connections.length,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RequestItem extends StatelessWidget {
|
||||
final Connection connection;
|
||||
final Function(String)? onClick;
|
||||
|
||||
const RequestItem({
|
||||
super.key,
|
||||
required this.connection,
|
||||
this.onClick,
|
||||
});
|
||||
|
||||
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
|
||||
@@ -213,8 +119,8 @@ class RequestItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem(
|
||||
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||
return ListTile(
|
||||
titleAlignment: ListTileTitleAlignment.top,
|
||||
leading: Platform.isAndroid
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
@@ -259,10 +165,6 @@ class RequestItem extends StatelessWidget {
|
||||
for (final chain in connection.chains)
|
||||
CommonChip(
|
||||
label: chain,
|
||||
onPressed: () {
|
||||
if (onClick == null) return;
|
||||
onClick!(chain);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -274,144 +176,3 @@ class RequestItem extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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: 8,
|
||||
spacing: 8,
|
||||
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 RequestItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
_addKeyword(value);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: _results.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +78,9 @@ class _ResourcesState extends State<Resources> {
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 12,right: 4),
|
||||
child: VerticalDivider(
|
||||
endIndent: 6,
|
||||
endIndent: 2,
|
||||
width: 4,
|
||||
indent: 6,
|
||||
indent: 2,
|
||||
),
|
||||
),
|
||||
externalProvider.vehicleType == "HTTP"
|
||||
|
||||
@@ -179,18 +179,10 @@
|
||||
"geodataLoaderDesc": "Enabling will use the Geo low memory loader",
|
||||
"requests": "Requests",
|
||||
"requestsDesc": "View recently requested data",
|
||||
"nullRequestsDesc": "No proxy or no request",
|
||||
"findProcessMode": "Find process",
|
||||
"findProcessModeDesc": "There is a risk of flashback after opening",
|
||||
"init": "Init",
|
||||
"infiniteTime": "Long term effective",
|
||||
"expirationTime": "Expiration time",
|
||||
"connections": "Connections",
|
||||
"connectionsDesc": "View current connection",
|
||||
"nullRequestsDesc": "No requests",
|
||||
"nullConnectionsDesc": "No connections",
|
||||
"intranetIp": "Intranet IP",
|
||||
"view": "View",
|
||||
"cut": "Cut",
|
||||
"copy": "Copy",
|
||||
"paste": "Paste"
|
||||
"expirationTime": "Expiration time"
|
||||
}
|
||||
@@ -179,18 +179,10 @@
|
||||
"geodataLoaderDesc": "开启将使用Geo低内存加载器",
|
||||
"requests": "请求",
|
||||
"requestsDesc": "查看最近请求数据",
|
||||
"nullRequestsDesc": "未开启代理或者没有请求",
|
||||
"findProcessMode": "查找进程",
|
||||
"findProcessModeDesc": "开启后存在闪退风险",
|
||||
"init": "初始化",
|
||||
"infiniteTime": "长期有效",
|
||||
"expirationTime": "到期时间",
|
||||
"connections": "连接",
|
||||
"connectionsDesc": "查看当前连接",
|
||||
"nullRequestsDesc": "暂无请求",
|
||||
"nullConnectionsDesc": "暂无连接",
|
||||
"intranetIp": "内网 IP",
|
||||
"view": "查看",
|
||||
"cut": "剪切",
|
||||
"copy": "复制",
|
||||
"paste": "粘贴"
|
||||
"expirationTime": "到期时间"
|
||||
}
|
||||
@@ -92,16 +92,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"compatibleDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Opening it will lose part of its application ability and gain the support of full amount of Clash."),
|
||||
"confirm": MessageLookupByLibrary.simpleMessage("Confirm"),
|
||||
"connections": MessageLookupByLibrary.simpleMessage("Connections"),
|
||||
"connectionsDesc":
|
||||
MessageLookupByLibrary.simpleMessage("View current connection"),
|
||||
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity:"),
|
||||
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
|
||||
"core": MessageLookupByLibrary.simpleMessage("Core"),
|
||||
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("Country"),
|
||||
"create": MessageLookupByLibrary.simpleMessage("Create"),
|
||||
"cut": MessageLookupByLibrary.simpleMessage("Cut"),
|
||||
"dark": MessageLookupByLibrary.simpleMessage("Dark"),
|
||||
"dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"),
|
||||
"days": MessageLookupByLibrary.simpleMessage("Days"),
|
||||
@@ -152,7 +147,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"infiniteTime":
|
||||
MessageLookupByLibrary.simpleMessage("Long term effective"),
|
||||
"init": MessageLookupByLibrary.simpleMessage("Init"),
|
||||
"intranetIp": MessageLookupByLibrary.simpleMessage("Intranet IP"),
|
||||
"ipCheckTimeout":
|
||||
MessageLookupByLibrary.simpleMessage("Ip check timeout"),
|
||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -185,14 +179,13 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Please create a profile or add a valid profile"),
|
||||
"notSelectedTip": MessageLookupByLibrary.simpleMessage(
|
||||
"The current proxy group cannot be selected."),
|
||||
"nullConnectionsDesc":
|
||||
MessageLookupByLibrary.simpleMessage("No connections"),
|
||||
"nullCoreInfoDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Unable to obtain core info"),
|
||||
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("No logs"),
|
||||
"nullProfileDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"No profile, Please add a profile"),
|
||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
|
||||
"nullRequestsDesc":
|
||||
MessageLookupByLibrary.simpleMessage("No proxy or no request"),
|
||||
"other": MessageLookupByLibrary.simpleMessage("Other"),
|
||||
"outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"),
|
||||
"override": MessageLookupByLibrary.simpleMessage("Override"),
|
||||
@@ -201,7 +194,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"password": MessageLookupByLibrary.simpleMessage("Password"),
|
||||
"passwordTip":
|
||||
MessageLookupByLibrary.simpleMessage("Password cannot be empty"),
|
||||
"paste": MessageLookupByLibrary.simpleMessage("Paste"),
|
||||
"pleaseBindWebDAV":
|
||||
MessageLookupByLibrary.simpleMessage("Please bind WebDAV"),
|
||||
"pleaseUploadFile":
|
||||
@@ -294,7 +286,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"url": MessageLookupByLibrary.simpleMessage("URL"),
|
||||
"urlDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Obtain profile through URL"),
|
||||
"view": MessageLookupByLibrary.simpleMessage("View"),
|
||||
"webDAVConfiguration":
|
||||
MessageLookupByLibrary.simpleMessage("WebDAV configuration"),
|
||||
"whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"),
|
||||
|
||||
@@ -75,15 +75,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"compatibleDesc":
|
||||
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力,获得全量的Clash的支持"),
|
||||
"confirm": MessageLookupByLibrary.simpleMessage("确定"),
|
||||
"connections": MessageLookupByLibrary.simpleMessage("连接"),
|
||||
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接"),
|
||||
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
|
||||
"copy": MessageLookupByLibrary.simpleMessage("复制"),
|
||||
"core": MessageLookupByLibrary.simpleMessage("内核"),
|
||||
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("区域"),
|
||||
"create": MessageLookupByLibrary.simpleMessage("创建"),
|
||||
"cut": MessageLookupByLibrary.simpleMessage("剪切"),
|
||||
"dark": MessageLookupByLibrary.simpleMessage("深色"),
|
||||
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
|
||||
"days": MessageLookupByLibrary.simpleMessage("天"),
|
||||
@@ -123,7 +119,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
|
||||
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
|
||||
"init": MessageLookupByLibrary.simpleMessage("初始化"),
|
||||
"intranetIp": MessageLookupByLibrary.simpleMessage("内网 IP"),
|
||||
"ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
|
||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收ipv6流量"),
|
||||
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
|
||||
@@ -150,19 +145,17 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"noProxyDesc":
|
||||
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
|
||||
"notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"),
|
||||
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"),
|
||||
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"),
|
||||
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
|
||||
"nullProfileDesc":
|
||||
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
|
||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
|
||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("未开启代理或者没有请求"),
|
||||
"other": MessageLookupByLibrary.simpleMessage("其他"),
|
||||
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
|
||||
"override": MessageLookupByLibrary.simpleMessage("覆写"),
|
||||
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
|
||||
"password": MessageLookupByLibrary.simpleMessage("密码"),
|
||||
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
|
||||
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
|
||||
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
|
||||
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
|
||||
"pleaseUploadValidQrcode":
|
||||
@@ -236,7 +229,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"upload": MessageLookupByLibrary.simpleMessage("上传"),
|
||||
"url": MessageLookupByLibrary.simpleMessage("URL"),
|
||||
"urlDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
|
||||
"view": MessageLookupByLibrary.simpleMessage("查看"),
|
||||
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
|
||||
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),
|
||||
"years": MessageLookupByLibrary.simpleMessage("年"),
|
||||
|
||||
@@ -1850,6 +1850,16 @@ class AppLocalizations {
|
||||
);
|
||||
}
|
||||
|
||||
/// `No proxy or no request`
|
||||
String get nullRequestsDesc {
|
||||
return Intl.message(
|
||||
'No proxy or no request',
|
||||
name: 'nullRequestsDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Find process`
|
||||
String get findProcessMode {
|
||||
return Intl.message(
|
||||
@@ -1899,96 +1909,6 @@ class AppLocalizations {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Connections`
|
||||
String get connections {
|
||||
return Intl.message(
|
||||
'Connections',
|
||||
name: 'connections',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `View current connection`
|
||||
String get connectionsDesc {
|
||||
return Intl.message(
|
||||
'View current connection',
|
||||
name: 'connectionsDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `No requests`
|
||||
String get nullRequestsDesc {
|
||||
return Intl.message(
|
||||
'No requests',
|
||||
name: 'nullRequestsDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `No connections`
|
||||
String get nullConnectionsDesc {
|
||||
return Intl.message(
|
||||
'No connections',
|
||||
name: 'nullConnectionsDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Intranet IP`
|
||||
String get intranetIp {
|
||||
return Intl.message(
|
||||
'Intranet IP',
|
||||
name: 'intranetIp',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `View`
|
||||
String get view {
|
||||
return Intl.message(
|
||||
'View',
|
||||
name: 'view',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Cut`
|
||||
String get cut {
|
||||
return Intl.message(
|
||||
'Cut',
|
||||
name: 'cut',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Copy`
|
||||
String get copy {
|
||||
return Intl.message(
|
||||
'Copy',
|
||||
name: 'copy',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Paste`
|
||||
String get paste {
|
||||
return Intl.message(
|
||||
'Paste',
|
||||
name: 'paste',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
|
||||
@@ -61,7 +61,7 @@ Future<void> vpnService() async {
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
|
||||
globalState.applyProfile(
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
|
||||
@@ -187,7 +187,7 @@ class AppState with ChangeNotifier {
|
||||
}
|
||||
|
||||
addRequest(Connection value) {
|
||||
_requests = List.from(_requests)..add(value);
|
||||
_requests.add(value);
|
||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||
if (_requests.length > maxLength) {
|
||||
_requests = _requests.sublist(_requests.length - maxLength);
|
||||
@@ -205,7 +205,7 @@ class AppState with ChangeNotifier {
|
||||
}
|
||||
|
||||
addLog(Log log) {
|
||||
_logs = List.from(_logs)..add(log);
|
||||
_logs.add(log);
|
||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||
if (_logs.length > maxLength) {
|
||||
_logs = _logs.sublist(_logs.length - maxLength);
|
||||
|
||||
@@ -143,21 +143,18 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Profile? get currentProfile {
|
||||
final index =
|
||||
profiles.indexWhere((profile) => profile.id == _currentProfileId);
|
||||
return index == -1 ? null : profiles[index];
|
||||
try {
|
||||
return profiles.firstWhere((element) => element.id == _currentProfileId);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? get currentGroupName => currentProfile?.currentGroupName;
|
||||
|
||||
updateCurrentGroupName(String groupName) {
|
||||
if (currentProfile != null &&
|
||||
currentProfile!.currentGroupName != groupName) {
|
||||
_setProfile(
|
||||
currentProfile!.copyWith(
|
||||
currentGroupName: groupName,
|
||||
),
|
||||
);
|
||||
if (currentProfile?.currentGroupName != groupName) {
|
||||
currentProfile?.currentGroupName = groupName;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -167,16 +164,9 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
|
||||
updateCurrentSelectedMap(String groupName, String proxyName) {
|
||||
if (currentProfile != null &&
|
||||
currentProfile!.selectedMap[groupName] != proxyName) {
|
||||
final SelectedMap selectedMap = Map.from(
|
||||
currentProfile?.selectedMap ?? {},
|
||||
)..[groupName] = proxyName;
|
||||
_setProfile(
|
||||
currentProfile!.copyWith(
|
||||
selectedMap: selectedMap,
|
||||
),
|
||||
);
|
||||
if (currentProfile?.selectedMap[groupName] != proxyName) {
|
||||
currentProfile?.selectedMap = Map.from(currentProfile?.selectedMap ?? {})
|
||||
..[groupName] = proxyName;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class Metadata with _$Metadata {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Connection with _$Connection {
|
||||
class Connection with _$Connection{
|
||||
const factory Connection({
|
||||
required String id,
|
||||
num? upload,
|
||||
@@ -36,19 +36,3 @@ class Connection with _$Connection {
|
||||
factory Connection.fromJson(Map<String, Object?> json) =>
|
||||
_$ConnectionFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ConnectionsAndKeywords with _$ConnectionsAndKeywords {
|
||||
const factory ConnectionsAndKeywords({
|
||||
@Default([]) List<Connection> connections,
|
||||
@Default([]) List<String> keywords,
|
||||
}) = _ConnectionsAndKeywords;
|
||||
|
||||
factory ConnectionsAndKeywords.fromJson(Map<String, Object?> json) =>
|
||||
_$ConnectionsAndKeywordsFromJson(json);
|
||||
}
|
||||
|
||||
|
||||
extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords{
|
||||
List<Connection> get filteredConnections => connections.where((connection)=> Set.from(connection.chains).containsAll(keywords)).toList();
|
||||
}
|
||||
@@ -589,184 +589,3 @@ abstract class _Connection implements Connection {
|
||||
_$$ConnectionImplCopyWith<_$ConnectionImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
ConnectionsAndKeywords _$ConnectionsAndKeywordsFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return _ConnectionsAndKeywords.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ConnectionsAndKeywords {
|
||||
List<Connection> get connections => throw _privateConstructorUsedError;
|
||||
List<String> get keywords => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$ConnectionsAndKeywordsCopyWith<ConnectionsAndKeywords> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ConnectionsAndKeywordsCopyWith<$Res> {
|
||||
factory $ConnectionsAndKeywordsCopyWith(ConnectionsAndKeywords value,
|
||||
$Res Function(ConnectionsAndKeywords) then) =
|
||||
_$ConnectionsAndKeywordsCopyWithImpl<$Res, ConnectionsAndKeywords>;
|
||||
@useResult
|
||||
$Res call({List<Connection> connections, List<String> keywords});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ConnectionsAndKeywordsCopyWithImpl<$Res,
|
||||
$Val extends ConnectionsAndKeywords>
|
||||
implements $ConnectionsAndKeywordsCopyWith<$Res> {
|
||||
_$ConnectionsAndKeywordsCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? connections = null,
|
||||
Object? keywords = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
connections: null == connections
|
||||
? _value.connections
|
||||
: connections // ignore: cast_nullable_to_non_nullable
|
||||
as List<Connection>,
|
||||
keywords: null == keywords
|
||||
? _value.keywords
|
||||
: keywords // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$ConnectionsAndKeywordsImplCopyWith<$Res>
|
||||
implements $ConnectionsAndKeywordsCopyWith<$Res> {
|
||||
factory _$$ConnectionsAndKeywordsImplCopyWith(
|
||||
_$ConnectionsAndKeywordsImpl value,
|
||||
$Res Function(_$ConnectionsAndKeywordsImpl) then) =
|
||||
__$$ConnectionsAndKeywordsImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({List<Connection> connections, List<String> keywords});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$ConnectionsAndKeywordsImplCopyWithImpl<$Res>
|
||||
extends _$ConnectionsAndKeywordsCopyWithImpl<$Res,
|
||||
_$ConnectionsAndKeywordsImpl>
|
||||
implements _$$ConnectionsAndKeywordsImplCopyWith<$Res> {
|
||||
__$$ConnectionsAndKeywordsImplCopyWithImpl(
|
||||
_$ConnectionsAndKeywordsImpl _value,
|
||||
$Res Function(_$ConnectionsAndKeywordsImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? connections = null,
|
||||
Object? keywords = null,
|
||||
}) {
|
||||
return _then(_$ConnectionsAndKeywordsImpl(
|
||||
connections: null == connections
|
||||
? _value._connections
|
||||
: connections // ignore: cast_nullable_to_non_nullable
|
||||
as List<Connection>,
|
||||
keywords: null == keywords
|
||||
? _value._keywords
|
||||
: keywords // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$ConnectionsAndKeywordsImpl implements _ConnectionsAndKeywords {
|
||||
const _$ConnectionsAndKeywordsImpl(
|
||||
{final List<Connection> connections = const [],
|
||||
final List<String> keywords = const []})
|
||||
: _connections = connections,
|
||||
_keywords = keywords;
|
||||
|
||||
factory _$ConnectionsAndKeywordsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$ConnectionsAndKeywordsImplFromJson(json);
|
||||
|
||||
final List<Connection> _connections;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<Connection> get connections {
|
||||
if (_connections is EqualUnmodifiableListView) return _connections;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_connections);
|
||||
}
|
||||
|
||||
final List<String> _keywords;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<String> get keywords {
|
||||
if (_keywords is EqualUnmodifiableListView) return _keywords;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_keywords);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConnectionsAndKeywords(connections: $connections, keywords: $keywords)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$ConnectionsAndKeywordsImpl &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._connections, _connections) &&
|
||||
const DeepCollectionEquality().equals(other._keywords, _keywords));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(_connections),
|
||||
const DeepCollectionEquality().hash(_keywords));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$ConnectionsAndKeywordsImplCopyWith<_$ConnectionsAndKeywordsImpl>
|
||||
get copyWith => __$$ConnectionsAndKeywordsImplCopyWithImpl<
|
||||
_$ConnectionsAndKeywordsImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$ConnectionsAndKeywordsImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _ConnectionsAndKeywords implements ConnectionsAndKeywords {
|
||||
const factory _ConnectionsAndKeywords(
|
||||
{final List<Connection> connections,
|
||||
final List<String> keywords}) = _$ConnectionsAndKeywordsImpl;
|
||||
|
||||
factory _ConnectionsAndKeywords.fromJson(Map<String, dynamic> json) =
|
||||
_$ConnectionsAndKeywordsImpl.fromJson;
|
||||
|
||||
@override
|
||||
List<Connection> get connections;
|
||||
@override
|
||||
List<String> get keywords;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$ConnectionsAndKeywordsImplCopyWith<_$ConnectionsAndKeywordsImpl>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
@@ -52,23 +52,3 @@ Map<String, dynamic> _$$ConnectionImplToJson(_$ConnectionImpl instance) =>
|
||||
'metadata': instance.metadata,
|
||||
'chains': instance.chains,
|
||||
};
|
||||
|
||||
_$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,
|
||||
};
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of '../log.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
LogsAndKeywords _$LogsAndKeywordsFromJson(Map<String, dynamic> json) {
|
||||
return _LogsAndKeywords.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LogsAndKeywords {
|
||||
List<Log> get logs => throw _privateConstructorUsedError;
|
||||
List<String> get keywords => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$LogsAndKeywordsCopyWith<LogsAndKeywords> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $LogsAndKeywordsCopyWith<$Res> {
|
||||
factory $LogsAndKeywordsCopyWith(
|
||||
LogsAndKeywords value, $Res Function(LogsAndKeywords) then) =
|
||||
_$LogsAndKeywordsCopyWithImpl<$Res, LogsAndKeywords>;
|
||||
@useResult
|
||||
$Res call({List<Log> logs, List<String> keywords});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LogsAndKeywordsCopyWithImpl<$Res, $Val extends LogsAndKeywords>
|
||||
implements $LogsAndKeywordsCopyWith<$Res> {
|
||||
_$LogsAndKeywordsCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? logs = null,
|
||||
Object? keywords = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
logs: null == logs
|
||||
? _value.logs
|
||||
: logs // ignore: cast_nullable_to_non_nullable
|
||||
as List<Log>,
|
||||
keywords: null == keywords
|
||||
? _value.keywords
|
||||
: keywords // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$LogsAndKeywordsImplCopyWith<$Res>
|
||||
implements $LogsAndKeywordsCopyWith<$Res> {
|
||||
factory _$$LogsAndKeywordsImplCopyWith(_$LogsAndKeywordsImpl value,
|
||||
$Res Function(_$LogsAndKeywordsImpl) then) =
|
||||
__$$LogsAndKeywordsImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({List<Log> logs, List<String> keywords});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$LogsAndKeywordsImplCopyWithImpl<$Res>
|
||||
extends _$LogsAndKeywordsCopyWithImpl<$Res, _$LogsAndKeywordsImpl>
|
||||
implements _$$LogsAndKeywordsImplCopyWith<$Res> {
|
||||
__$$LogsAndKeywordsImplCopyWithImpl(
|
||||
_$LogsAndKeywordsImpl _value, $Res Function(_$LogsAndKeywordsImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? logs = null,
|
||||
Object? keywords = null,
|
||||
}) {
|
||||
return _then(_$LogsAndKeywordsImpl(
|
||||
logs: null == logs
|
||||
? _value._logs
|
||||
: logs // ignore: cast_nullable_to_non_nullable
|
||||
as List<Log>,
|
||||
keywords: null == keywords
|
||||
? _value._keywords
|
||||
: keywords // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$LogsAndKeywordsImpl implements _LogsAndKeywords {
|
||||
const _$LogsAndKeywordsImpl(
|
||||
{final List<Log> logs = const [], final List<String> keywords = const []})
|
||||
: _logs = logs,
|
||||
_keywords = keywords;
|
||||
|
||||
factory _$LogsAndKeywordsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$LogsAndKeywordsImplFromJson(json);
|
||||
|
||||
final List<Log> _logs;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<Log> get logs {
|
||||
if (_logs is EqualUnmodifiableListView) return _logs;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_logs);
|
||||
}
|
||||
|
||||
final List<String> _keywords;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<String> get keywords {
|
||||
if (_keywords is EqualUnmodifiableListView) return _keywords;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_keywords);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LogsAndKeywords(logs: $logs, keywords: $keywords)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$LogsAndKeywordsImpl &&
|
||||
const DeepCollectionEquality().equals(other._logs, _logs) &&
|
||||
const DeepCollectionEquality().equals(other._keywords, _keywords));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(_logs),
|
||||
const DeepCollectionEquality().hash(_keywords));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$LogsAndKeywordsImplCopyWith<_$LogsAndKeywordsImpl> get copyWith =>
|
||||
__$$LogsAndKeywordsImplCopyWithImpl<_$LogsAndKeywordsImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$LogsAndKeywordsImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _LogsAndKeywords implements LogsAndKeywords {
|
||||
const factory _LogsAndKeywords(
|
||||
{final List<Log> logs,
|
||||
final List<String> keywords}) = _$LogsAndKeywordsImpl;
|
||||
|
||||
factory _LogsAndKeywords.fromJson(Map<String, dynamic> json) =
|
||||
_$LogsAndKeywordsImpl.fromJson;
|
||||
|
||||
@override
|
||||
List<Log> get logs;
|
||||
@override
|
||||
List<String> get keywords;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$LogsAndKeywordsImplCopyWith<_$LogsAndKeywordsImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -23,23 +23,3 @@ const _$LogLevelEnumMap = {
|
||||
LogLevel.error: 'error',
|
||||
LogLevel.silent: 'silent',
|
||||
};
|
||||
|
||||
_$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,
|
||||
};
|
||||
|
||||
@@ -1,546 +0,0 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of '../profile.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
UserInfo _$UserInfoFromJson(Map<String, dynamic> json) {
|
||||
return _UserInfo.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UserInfo {
|
||||
int get upload => throw _privateConstructorUsedError;
|
||||
int get download => throw _privateConstructorUsedError;
|
||||
int get total => throw _privateConstructorUsedError;
|
||||
int get expire => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$UserInfoCopyWith<UserInfo> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $UserInfoCopyWith<$Res> {
|
||||
factory $UserInfoCopyWith(UserInfo value, $Res Function(UserInfo) then) =
|
||||
_$UserInfoCopyWithImpl<$Res, UserInfo>;
|
||||
@useResult
|
||||
$Res call({int upload, int download, int total, int expire});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$UserInfoCopyWithImpl<$Res, $Val extends UserInfo>
|
||||
implements $UserInfoCopyWith<$Res> {
|
||||
_$UserInfoCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? upload = null,
|
||||
Object? download = null,
|
||||
Object? total = null,
|
||||
Object? expire = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
upload: null == upload
|
||||
? _value.upload
|
||||
: upload // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
download: null == download
|
||||
? _value.download
|
||||
: download // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
total: null == total
|
||||
? _value.total
|
||||
: total // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
expire: null == expire
|
||||
? _value.expire
|
||||
: expire // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$UserInfoImplCopyWith<$Res>
|
||||
implements $UserInfoCopyWith<$Res> {
|
||||
factory _$$UserInfoImplCopyWith(
|
||||
_$UserInfoImpl value, $Res Function(_$UserInfoImpl) then) =
|
||||
__$$UserInfoImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({int upload, int download, int total, int expire});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$UserInfoImplCopyWithImpl<$Res>
|
||||
extends _$UserInfoCopyWithImpl<$Res, _$UserInfoImpl>
|
||||
implements _$$UserInfoImplCopyWith<$Res> {
|
||||
__$$UserInfoImplCopyWithImpl(
|
||||
_$UserInfoImpl _value, $Res Function(_$UserInfoImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? upload = null,
|
||||
Object? download = null,
|
||||
Object? total = null,
|
||||
Object? expire = null,
|
||||
}) {
|
||||
return _then(_$UserInfoImpl(
|
||||
upload: null == upload
|
||||
? _value.upload
|
||||
: upload // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
download: null == download
|
||||
? _value.download
|
||||
: download // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
total: null == total
|
||||
? _value.total
|
||||
: total // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
expire: null == expire
|
||||
? _value.expire
|
||||
: expire // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$UserInfoImpl implements _UserInfo {
|
||||
const _$UserInfoImpl(
|
||||
{this.upload = 0, this.download = 0, this.total = 0, this.expire = 0});
|
||||
|
||||
factory _$UserInfoImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$UserInfoImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final int upload;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int download;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int total;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int expire;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserInfo(upload: $upload, download: $download, total: $total, expire: $expire)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$UserInfoImpl &&
|
||||
(identical(other.upload, upload) || other.upload == upload) &&
|
||||
(identical(other.download, download) ||
|
||||
other.download == download) &&
|
||||
(identical(other.total, total) || other.total == total) &&
|
||||
(identical(other.expire, expire) || other.expire == expire));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, upload, download, total, expire);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$UserInfoImplCopyWith<_$UserInfoImpl> get copyWith =>
|
||||
__$$UserInfoImplCopyWithImpl<_$UserInfoImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$UserInfoImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _UserInfo implements UserInfo {
|
||||
const factory _UserInfo(
|
||||
{final int upload,
|
||||
final int download,
|
||||
final int total,
|
||||
final int expire}) = _$UserInfoImpl;
|
||||
|
||||
factory _UserInfo.fromJson(Map<String, dynamic> json) =
|
||||
_$UserInfoImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get upload;
|
||||
@override
|
||||
int get download;
|
||||
@override
|
||||
int get total;
|
||||
@override
|
||||
int get expire;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$UserInfoImplCopyWith<_$UserInfoImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
Profile _$ProfileFromJson(Map<String, dynamic> json) {
|
||||
return _Profile.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Profile {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String? get label => throw _privateConstructorUsedError;
|
||||
String? get currentGroupName => throw _privateConstructorUsedError;
|
||||
String get url => throw _privateConstructorUsedError;
|
||||
DateTime? get lastUpdateDate => throw _privateConstructorUsedError;
|
||||
Duration get autoUpdateDuration => throw _privateConstructorUsedError;
|
||||
UserInfo? get userInfo => throw _privateConstructorUsedError;
|
||||
bool get autoUpdate => throw _privateConstructorUsedError;
|
||||
Map<String, String> get selectedMap => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$ProfileCopyWith<Profile> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ProfileCopyWith<$Res> {
|
||||
factory $ProfileCopyWith(Profile value, $Res Function(Profile) then) =
|
||||
_$ProfileCopyWithImpl<$Res, Profile>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String id,
|
||||
String? label,
|
||||
String? currentGroupName,
|
||||
String url,
|
||||
DateTime? lastUpdateDate,
|
||||
Duration autoUpdateDuration,
|
||||
UserInfo? userInfo,
|
||||
bool autoUpdate,
|
||||
Map<String, String> selectedMap});
|
||||
|
||||
$UserInfoCopyWith<$Res>? get userInfo;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ProfileCopyWithImpl<$Res, $Val extends Profile>
|
||||
implements $ProfileCopyWith<$Res> {
|
||||
_$ProfileCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? label = freezed,
|
||||
Object? currentGroupName = freezed,
|
||||
Object? url = null,
|
||||
Object? lastUpdateDate = freezed,
|
||||
Object? autoUpdateDuration = null,
|
||||
Object? userInfo = freezed,
|
||||
Object? autoUpdate = null,
|
||||
Object? selectedMap = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
label: freezed == label
|
||||
? _value.label
|
||||
: label // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
currentGroupName: freezed == currentGroupName
|
||||
? _value.currentGroupName
|
||||
: currentGroupName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
lastUpdateDate: freezed == lastUpdateDate
|
||||
? _value.lastUpdateDate
|
||||
: lastUpdateDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
autoUpdateDuration: null == autoUpdateDuration
|
||||
? _value.autoUpdateDuration
|
||||
: autoUpdateDuration // ignore: cast_nullable_to_non_nullable
|
||||
as Duration,
|
||||
userInfo: freezed == userInfo
|
||||
? _value.userInfo
|
||||
: userInfo // ignore: cast_nullable_to_non_nullable
|
||||
as UserInfo?,
|
||||
autoUpdate: null == autoUpdate
|
||||
? _value.autoUpdate
|
||||
: autoUpdate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
selectedMap: null == selectedMap
|
||||
? _value.selectedMap
|
||||
: selectedMap // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$UserInfoCopyWith<$Res>? get userInfo {
|
||||
if (_value.userInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $UserInfoCopyWith<$Res>(_value.userInfo!, (value) {
|
||||
return _then(_value.copyWith(userInfo: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$ProfileImplCopyWith<$Res> implements $ProfileCopyWith<$Res> {
|
||||
factory _$$ProfileImplCopyWith(
|
||||
_$ProfileImpl value, $Res Function(_$ProfileImpl) then) =
|
||||
__$$ProfileImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String id,
|
||||
String? label,
|
||||
String? currentGroupName,
|
||||
String url,
|
||||
DateTime? lastUpdateDate,
|
||||
Duration autoUpdateDuration,
|
||||
UserInfo? userInfo,
|
||||
bool autoUpdate,
|
||||
Map<String, String> selectedMap});
|
||||
|
||||
@override
|
||||
$UserInfoCopyWith<$Res>? get userInfo;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$ProfileImplCopyWithImpl<$Res>
|
||||
extends _$ProfileCopyWithImpl<$Res, _$ProfileImpl>
|
||||
implements _$$ProfileImplCopyWith<$Res> {
|
||||
__$$ProfileImplCopyWithImpl(
|
||||
_$ProfileImpl _value, $Res Function(_$ProfileImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? label = freezed,
|
||||
Object? currentGroupName = freezed,
|
||||
Object? url = null,
|
||||
Object? lastUpdateDate = freezed,
|
||||
Object? autoUpdateDuration = null,
|
||||
Object? userInfo = freezed,
|
||||
Object? autoUpdate = null,
|
||||
Object? selectedMap = null,
|
||||
}) {
|
||||
return _then(_$ProfileImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
label: freezed == label
|
||||
? _value.label
|
||||
: label // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
currentGroupName: freezed == currentGroupName
|
||||
? _value.currentGroupName
|
||||
: currentGroupName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
lastUpdateDate: freezed == lastUpdateDate
|
||||
? _value.lastUpdateDate
|
||||
: lastUpdateDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
autoUpdateDuration: null == autoUpdateDuration
|
||||
? _value.autoUpdateDuration
|
||||
: autoUpdateDuration // ignore: cast_nullable_to_non_nullable
|
||||
as Duration,
|
||||
userInfo: freezed == userInfo
|
||||
? _value.userInfo
|
||||
: userInfo // ignore: cast_nullable_to_non_nullable
|
||||
as UserInfo?,
|
||||
autoUpdate: null == autoUpdate
|
||||
? _value.autoUpdate
|
||||
: autoUpdate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
selectedMap: null == selectedMap
|
||||
? _value._selectedMap
|
||||
: selectedMap // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$ProfileImpl implements _Profile {
|
||||
const _$ProfileImpl(
|
||||
{required this.id,
|
||||
this.label,
|
||||
this.currentGroupName,
|
||||
this.url = "",
|
||||
this.lastUpdateDate,
|
||||
required this.autoUpdateDuration,
|
||||
this.userInfo,
|
||||
this.autoUpdate = true,
|
||||
final Map<String, String> selectedMap = const {}})
|
||||
: _selectedMap = selectedMap;
|
||||
|
||||
factory _$ProfileImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$ProfileImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final String? label;
|
||||
@override
|
||||
final String? currentGroupName;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String url;
|
||||
@override
|
||||
final DateTime? lastUpdateDate;
|
||||
@override
|
||||
final Duration autoUpdateDuration;
|
||||
@override
|
||||
final UserInfo? userInfo;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool autoUpdate;
|
||||
final Map<String, String> _selectedMap;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<String, String> get selectedMap {
|
||||
if (_selectedMap is EqualUnmodifiableMapView) return _selectedMap;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_selectedMap);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$ProfileImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.label, label) || other.label == label) &&
|
||||
(identical(other.currentGroupName, currentGroupName) ||
|
||||
other.currentGroupName == currentGroupName) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(identical(other.lastUpdateDate, lastUpdateDate) ||
|
||||
other.lastUpdateDate == lastUpdateDate) &&
|
||||
(identical(other.autoUpdateDuration, autoUpdateDuration) ||
|
||||
other.autoUpdateDuration == autoUpdateDuration) &&
|
||||
(identical(other.userInfo, userInfo) ||
|
||||
other.userInfo == userInfo) &&
|
||||
(identical(other.autoUpdate, autoUpdate) ||
|
||||
other.autoUpdate == autoUpdate) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._selectedMap, _selectedMap));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
label,
|
||||
currentGroupName,
|
||||
url,
|
||||
lastUpdateDate,
|
||||
autoUpdateDuration,
|
||||
userInfo,
|
||||
autoUpdate,
|
||||
const DeepCollectionEquality().hash(_selectedMap));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$ProfileImplCopyWith<_$ProfileImpl> get copyWith =>
|
||||
__$$ProfileImplCopyWithImpl<_$ProfileImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$ProfileImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Profile implements Profile {
|
||||
const factory _Profile(
|
||||
{required final String id,
|
||||
final String? label,
|
||||
final String? currentGroupName,
|
||||
final String url,
|
||||
final DateTime? lastUpdateDate,
|
||||
required final Duration autoUpdateDuration,
|
||||
final UserInfo? userInfo,
|
||||
final bool autoUpdate,
|
||||
final Map<String, String> selectedMap}) = _$ProfileImpl;
|
||||
|
||||
factory _Profile.fromJson(Map<String, dynamic> json) = _$ProfileImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@override
|
||||
String? get label;
|
||||
@override
|
||||
String? get currentGroupName;
|
||||
@override
|
||||
String get url;
|
||||
@override
|
||||
DateTime? get lastUpdateDate;
|
||||
@override
|
||||
Duration get autoUpdateDuration;
|
||||
@override
|
||||
UserInfo? get userInfo;
|
||||
@override
|
||||
bool get autoUpdate;
|
||||
@override
|
||||
Map<String, String> get selectedMap;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$ProfileImplCopyWith<_$ProfileImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -6,45 +6,41 @@ part of '../profile.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$UserInfoImpl _$$UserInfoImplFromJson(Map<String, dynamic> json) =>
|
||||
_$UserInfoImpl(
|
||||
upload: (json['upload'] as num?)?.toInt() ?? 0,
|
||||
download: (json['download'] as num?)?.toInt() ?? 0,
|
||||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||
expire: (json['expire'] as num?)?.toInt() ?? 0,
|
||||
UserInfo _$UserInfoFromJson(Map<String, dynamic> json) => UserInfo(
|
||||
upload: (json['upload'] as num?)?.toInt(),
|
||||
download: (json['download'] as num?)?.toInt(),
|
||||
total: (json['total'] as num?)?.toInt(),
|
||||
expire: (json['expire'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$UserInfoImplToJson(_$UserInfoImpl instance) =>
|
||||
<String, dynamic>{
|
||||
Map<String, dynamic> _$UserInfoToJson(UserInfo instance) => <String, dynamic>{
|
||||
'upload': instance.upload,
|
||||
'download': instance.download,
|
||||
'total': instance.total,
|
||||
'expire': instance.expire,
|
||||
};
|
||||
|
||||
_$ProfileImpl _$$ProfileImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ProfileImpl(
|
||||
id: json['id'] as String,
|
||||
Profile _$ProfileFromJson(Map<String, dynamic> json) => Profile(
|
||||
id: json['id'] as String?,
|
||||
label: json['label'] as String?,
|
||||
url: json['url'] as String?,
|
||||
currentGroupName: json['currentGroupName'] as String?,
|
||||
url: json['url'] as String? ?? "",
|
||||
lastUpdateDate: json['lastUpdateDate'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastUpdateDate'] as String),
|
||||
autoUpdateDuration:
|
||||
Duration(microseconds: (json['autoUpdateDuration'] as num).toInt()),
|
||||
userInfo: json['userInfo'] == null
|
||||
? null
|
||||
: UserInfo.fromJson(json['userInfo'] as Map<String, dynamic>),
|
||||
autoUpdate: json['autoUpdate'] as bool? ?? true,
|
||||
lastUpdateDate: json['lastUpdateDate'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastUpdateDate'] as String),
|
||||
selectedMap: (json['selectedMap'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
) ??
|
||||
const {},
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
autoUpdateDuration: json['autoUpdateDuration'] == null
|
||||
? null
|
||||
: Duration(microseconds: (json['autoUpdateDuration'] as num).toInt()),
|
||||
autoUpdate: json['autoUpdate'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ProfileImplToJson(_$ProfileImpl instance) =>
|
||||
<String, dynamic>{
|
||||
Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'label': instance.label,
|
||||
'currentGroupName': instance.currentGroupName,
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// ignore_for_file: invalid_annotation_target
|
||||
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'generated/log.g.dart';
|
||||
|
||||
part 'generated/log.freezed.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Log {
|
||||
@JsonKey(name: "LogLevel")
|
||||
@@ -35,22 +31,3 @@ class Log {
|
||||
return 'Log{logLevel: $logLevel, payload: $payload, dateTime: $dateTime}';
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LogsAndKeywords with _$LogsAndKeywords {
|
||||
const factory LogsAndKeywords({
|
||||
@Default([]) List<Log> logs,
|
||||
@Default([]) List<String> keywords,
|
||||
}) = _LogsAndKeywords;
|
||||
|
||||
factory LogsAndKeywords.fromJson(Map<String, Object?> json) =>
|
||||
_$LogsAndKeywordsFromJson(json);
|
||||
}
|
||||
|
||||
extension LogsAndKeywordsExt on LogsAndKeywords {
|
||||
List<Log> get filteredLogs => logs
|
||||
.where(
|
||||
(log) => {log.logLevel.name}.containsAll(keywords),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -5,28 +5,39 @@ import 'dart:typed_data';
|
||||
import 'package:fl_clash/clash/core.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'generated/profile.g.dart';
|
||||
|
||||
part 'generated/profile.freezed.dart';
|
||||
|
||||
typedef SelectedMap = Map<String, String>;
|
||||
|
||||
@freezed
|
||||
class UserInfo with _$UserInfo {
|
||||
const factory UserInfo({
|
||||
@Default(0) int upload,
|
||||
@Default(0) int download,
|
||||
@Default(0) int total,
|
||||
@Default(0) int expire,
|
||||
}) = _UserInfo;
|
||||
@JsonSerializable()
|
||||
class UserInfo {
|
||||
int upload;
|
||||
int download;
|
||||
int total;
|
||||
int expire;
|
||||
|
||||
factory UserInfo.fromJson(Map<String, Object?> json) =>
|
||||
_$UserInfoFromJson(json);
|
||||
UserInfo({
|
||||
int? upload,
|
||||
int? download,
|
||||
int? total,
|
||||
int? expire,
|
||||
}) : upload = upload ?? 0,
|
||||
download = download ?? 0,
|
||||
total = total ?? 0,
|
||||
expire = expire ?? 0;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$UserInfoToJson(this);
|
||||
}
|
||||
|
||||
factory UserInfo.fromJson(Map<String, dynamic> json) {
|
||||
return _$UserInfoFromJson(json);
|
||||
}
|
||||
|
||||
factory UserInfo.formHString(String? info) {
|
||||
if (info == null) return const UserInfo();
|
||||
if (info == null) return UserInfo();
|
||||
final list = info.split(";");
|
||||
Map<String, int?> map = {};
|
||||
for (final i in list) {
|
||||
@@ -34,76 +45,73 @@ class UserInfo with _$UserInfo {
|
||||
map[keyValue[0]] = int.tryParse(keyValue[1]);
|
||||
}
|
||||
return UserInfo(
|
||||
upload: map["upload"] ?? 0,
|
||||
download: map["download"] ?? 0,
|
||||
total: map["total"] ?? 0,
|
||||
expire: map["expire"] ?? 0,
|
||||
upload: map["upload"],
|
||||
download: map["download"],
|
||||
total: map["total"],
|
||||
expire: map["expire"],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserInfo{upload: $upload, download: $download, total: $total, expire: $expire}';
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Profile with _$Profile {
|
||||
const factory Profile({
|
||||
required String id,
|
||||
String? label,
|
||||
String? currentGroupName,
|
||||
@Default("") String url,
|
||||
DateTime? lastUpdateDate,
|
||||
required Duration autoUpdateDuration,
|
||||
UserInfo? userInfo,
|
||||
@Default(true) bool autoUpdate,
|
||||
@Default({}) SelectedMap selectedMap,
|
||||
}) = _Profile;
|
||||
@JsonSerializable()
|
||||
class Profile {
|
||||
String id;
|
||||
String? label;
|
||||
String? currentGroupName;
|
||||
String? url;
|
||||
DateTime? lastUpdateDate;
|
||||
Duration autoUpdateDuration;
|
||||
UserInfo? userInfo;
|
||||
bool autoUpdate;
|
||||
SelectedMap selectedMap;
|
||||
|
||||
factory Profile.fromJson(Map<String, Object?> json) =>
|
||||
_$ProfileFromJson(json);
|
||||
Profile({
|
||||
String? id,
|
||||
this.label,
|
||||
this.url,
|
||||
this.currentGroupName,
|
||||
this.userInfo,
|
||||
this.lastUpdateDate,
|
||||
SelectedMap? selectedMap,
|
||||
Duration? autoUpdateDuration,
|
||||
this.autoUpdate = true,
|
||||
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
autoUpdateDuration = autoUpdateDuration ?? defaultUpdateDuration,
|
||||
selectedMap = selectedMap ?? {};
|
||||
|
||||
factory Profile.normal({
|
||||
String? label,
|
||||
String url = '',
|
||||
}) {
|
||||
return Profile(
|
||||
label: label,
|
||||
url: url,
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
autoUpdateDuration: defaultUpdateDuration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileExtension on Profile {
|
||||
ProfileType get type =>
|
||||
url.isEmpty == true ? ProfileType.file : ProfileType.url;
|
||||
|
||||
bool get realAutoUpdate =>
|
||||
url.isEmpty == true ? false : autoUpdate;
|
||||
url == null || url?.isEmpty == true ? ProfileType.file : ProfileType.url;
|
||||
|
||||
Future<void> checkAndUpdate() async {
|
||||
final isExists = await check();
|
||||
if (!isExists) {
|
||||
if (url.isNotEmpty) {
|
||||
await update();
|
||||
if (url != null) {
|
||||
return await update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> update() async {
|
||||
final response = await request.getFileResponseForUrl(url!);
|
||||
final disposition = response.headers.value("content-disposition");
|
||||
label ??= other.getFileNameForDisposition(disposition) ?? id;
|
||||
final userinfo = response.headers.value('subscription-userinfo');
|
||||
userInfo = UserInfo.formHString(userinfo);
|
||||
await saveFile(response.data);
|
||||
lastUpdateDate = DateTime.now();
|
||||
}
|
||||
|
||||
Future<bool> check() async {
|
||||
final profilePath = await appPath.getProfilePath(id);
|
||||
return await File(profilePath!).exists();
|
||||
}
|
||||
|
||||
Future<Profile> update() async {
|
||||
final response = await request.getFileResponseForUrl(url);
|
||||
final disposition = response.headers.value("content-disposition");
|
||||
final userinfo = response.headers.value('subscription-userinfo');
|
||||
return await copyWith(
|
||||
label: label ?? other.getFileNameForDisposition(disposition) ?? id,
|
||||
userInfo: UserInfo.formHString(userinfo),
|
||||
).saveFile(response.data);
|
||||
}
|
||||
|
||||
Future<Profile> saveFile(Uint8List bytes) async {
|
||||
Future<void> saveFile(Uint8List bytes) async {
|
||||
final message = await clashCore.validateConfig(utf8.decode(bytes));
|
||||
if (message.isNotEmpty) {
|
||||
throw message;
|
||||
@@ -115,21 +123,66 @@ extension ProfileExtension on Profile {
|
||||
await file.create(recursive: true);
|
||||
}
|
||||
await file.writeAsBytes(bytes);
|
||||
return copyWith(lastUpdateDate: DateTime.now());
|
||||
lastUpdateDate = DateTime.now();
|
||||
}
|
||||
|
||||
Future<Profile> saveFileWithString(String value) async {
|
||||
final message = await clashCore.validateConfig(value);
|
||||
if (message.isNotEmpty) {
|
||||
throw message;
|
||||
}
|
||||
final path = await appPath.getProfilePath(id);
|
||||
final file = File(path!);
|
||||
final isExists = await file.exists();
|
||||
if (!isExists) {
|
||||
await file.create(recursive: true);
|
||||
}
|
||||
await file.writeAsString(value);
|
||||
return copyWith(lastUpdateDate: DateTime.now());
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ProfileToJson(this);
|
||||
}
|
||||
|
||||
factory Profile.fromJson(Map<String, dynamic> json) {
|
||||
return _$ProfileFromJson(json);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Profile &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
label == other.label &&
|
||||
currentGroupName == other.currentGroupName &&
|
||||
url == other.url &&
|
||||
lastUpdateDate == other.lastUpdateDate &&
|
||||
autoUpdateDuration == other.autoUpdateDuration &&
|
||||
userInfo == other.userInfo &&
|
||||
autoUpdate == other.autoUpdate &&
|
||||
selectedMap == other.selectedMap;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
label.hashCode ^
|
||||
currentGroupName.hashCode ^
|
||||
url.hashCode ^
|
||||
lastUpdateDate.hashCode ^
|
||||
autoUpdateDuration.hashCode ^
|
||||
userInfo.hashCode ^
|
||||
autoUpdate.hashCode ^
|
||||
selectedMap.hashCode;
|
||||
|
||||
Profile copyWith({
|
||||
String? label,
|
||||
String? url,
|
||||
UserInfo? userInfo,
|
||||
String? currentGroupName,
|
||||
String? proxyName,
|
||||
DateTime? lastUpdateDate,
|
||||
Duration? autoUpdateDuration,
|
||||
bool? autoUpdate,
|
||||
SelectedMap? selectedMap,
|
||||
}) {
|
||||
return Profile(
|
||||
id: id,
|
||||
label: label ?? this.label,
|
||||
url: url ?? this.url,
|
||||
currentGroupName: currentGroupName ?? this.currentGroupName,
|
||||
userInfo: userInfo ?? this.userInfo,
|
||||
selectedMap: selectedMap ?? this.selectedMap,
|
||||
lastUpdateDate: lastUpdateDate ?? this.lastUpdateDate,
|
||||
autoUpdateDuration: autoUpdateDuration ?? this.autoUpdateDuration,
|
||||
autoUpdate: autoUpdate ?? this.autoUpdate,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,21 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CommonChip extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final ChipType type;
|
||||
final Widget? avatar;
|
||||
|
||||
const CommonChip({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.onPressed,
|
||||
this.avatar,
|
||||
this.type = ChipType.action,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if(type == ChipType.delete){
|
||||
return Chip(
|
||||
avatar: avatar,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onDeleted: onPressed ?? () {},
|
||||
side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)),
|
||||
labelStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
label: Text(label),
|
||||
);
|
||||
}
|
||||
return ActionChip(
|
||||
return Chip(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
avatar: avatar,
|
||||
onPressed: onPressed ?? () {},
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 0,
|
||||
horizontal: 4,
|
||||
),
|
||||
side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)),
|
||||
labelStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
label: Text(label),
|
||||
|
||||
@@ -11,7 +11,7 @@ class NullStatus extends StatelessWidget {
|
||||
return Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.titleMedium?.toBold,
|
||||
style: Theme.of(context).textTheme.titleMedium?.toBold(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,12 +53,6 @@ class _CommonPopupMenuState<T> extends State<CommonPopupMenu<T>> {
|
||||
widget.onSelected(value);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
groupValue.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<T>(
|
||||
|
||||
@@ -88,13 +88,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_actions.dispose();
|
||||
_floatingActionButton.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CommonScaffold oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
308
pubspec.lock
308
pubspec.lock
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
name: fl_clash
|
||||
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
||||
publish_to: 'none'
|
||||
version: 0.8.27
|
||||
version: 0.8.25
|
||||
environment:
|
||||
sdk: '>=3.1.0 <4.0.0'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user