diff --git a/core/common.go b/core/common.go index cedb251b..53ea93f8 100644 --- a/core/common.go +++ b/core/common.go @@ -423,6 +423,7 @@ func applyConfig() { if configParams.IsPatch { patchConfig(cfg.General) } else { + closeConnections() runtime.GC() hub.UltraApplyConfig(cfg, true) patchSelectGroup() diff --git a/core/hub.go b/core/hub.go index 4e8f6ac7..fe1076bc 100644 --- a/core/hub.go +++ b/core/hub.go @@ -299,7 +299,7 @@ func getConnections() *C.char { } //export closeConnections -func closeConnections() bool { +func closeConnections() { statistic.DefaultManager.Range(func(c statistic.Tracker) bool { err := c.Close() if err != nil { @@ -307,17 +307,16 @@ func closeConnections() bool { } return true }) - return true } //export closeConnection -func closeConnection(id *C.char) bool { +func closeConnection(id *C.char) { connectionId := C.GoString(id) - err := statistic.DefaultManager.Get(connectionId).Close() - if err != nil { - return false + c := statistic.DefaultManager.Get(connectionId) + if c == nil { + return } - return true + _ = c.Close() } //export getProviders diff --git a/lib/application.dart b/lib/application.dart index bd395ce7..e2533188 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -157,9 +157,7 @@ class ApplicationState extends State { GlobalWidgetsLocalizations.delegate ], builder: (_, child) { - return PopContainer( - child: _buildApp(child!), - ); + return _buildApp(child!); }, scrollBehavior: BaseScrollBehavior(), title: appName, diff --git a/lib/clash/core.dart b/lib/clash/core.dart index 207ff161..3ff7828d 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -319,11 +319,15 @@ class ClashCore { return connectionsRaw.map((e) => Connection.fromJson(e)).toList(); } - closeConnections(String id) { + closeConnection(String id) { final idChar = id.toNativeUtf8().cast(); clashFFI.closeConnection(idChar); malloc.free(idChar); } + + closeConnections() { + clashFFI.closeConnections(); + } } final clashCore = ClashCore(); diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart index 3917b36f..7a655e98 100644 --- a/lib/clash/generated/clash_ffi.dart +++ b/lib/clash/generated/clash_ffi.dart @@ -5351,16 +5351,16 @@ class ClashFFI { late final _getConnections = _getConnectionsPtr.asFunction Function()>(); - int closeConnections() { + void closeConnections() { return _closeConnections(); } late final _closeConnectionsPtr = - _lookup>('closeConnections'); + _lookup>('closeConnections'); late final _closeConnections = - _closeConnectionsPtr.asFunction(); + _closeConnectionsPtr.asFunction(); - int closeConnection( + void closeConnection( ffi.Pointer id, ) { return _closeConnection( @@ -5369,10 +5369,10 @@ class ClashFFI { } late final _closeConnectionPtr = - _lookup)>>( + _lookup)>>( 'closeConnection'); late final _closeConnection = - _closeConnectionPtr.asFunction)>(); + _closeConnectionPtr.asFunction)>(); ffi.Pointer getProviders() { return _getProviders(); diff --git a/lib/common/iterable.dart b/lib/common/iterable.dart index 079a1c1e..12355634 100644 --- a/lib/common/iterable.dart +++ b/lib/common/iterable.dart @@ -10,4 +10,58 @@ extension IterableExt on Iterable { yield iterator.current; } } + + Iterable> chunks(int size) sync* { + if (length == 0) return; + var iterator = this.iterator; + while (iterator.moveNext()) { + var chunk = [iterator.current]; + for (var i = 1; i < size && iterator.moveNext(); i++) { + chunk.add(iterator.current); + } + yield chunk; + } + } + + Iterable fill( + int length, { + required T Function(int count) filler, + }) sync* { + int count = 0; + for (var item in this) { + yield item; + count++; + if (count >= length) return; + } + while (count < length) { + yield filler(count); + count++; + } + } +} + +extension DoubleListExt on List { + int findInterval(num target) { + if (isEmpty) return -1; + if (target < first) return -1; + if (target >= last) return length - 1; + + int left = 0; + int right = length - 1; + + while (left <= right) { + int mid = left + (right - left) ~/ 2; + + if (mid == length - 1 || + (this[mid] <= target && target < this[mid + 1])) { + return mid; + } else if (target < this[mid]) { + right = mid - 1; + } else { + left = mid + 1; + } + } + + return -1; // 这行理论上不会执行到,但为了完整性保留 + } } diff --git a/lib/common/scroll.dart b/lib/common/scroll.dart index a5956209..054a0487 100644 --- a/lib/common/scroll.dart +++ b/lib/common/scroll.dart @@ -14,3 +14,14 @@ class BaseScrollBehavior extends MaterialScrollBehavior { PointerDeviceKind.unknown, }; } + +class HiddenBarScrollBehavior extends BaseScrollBehavior { + @override + Widget buildScrollbar( + BuildContext context, + Widget child, + ScrollableDetails details, + ) { + return child; + } +} diff --git a/lib/common/window.dart b/lib/common/window.dart index 9e038c62..2e35d659 100644 --- a/lib/common/window.dart +++ b/lib/common/window.dart @@ -19,7 +19,7 @@ class Window { await windowManager.ensureInitialized(); WindowOptions windowOptions = WindowOptions( size: Size(props.width, props.height), - minimumSize: const Size(380, 600), + minimumSize: const Size(380, 500), titleBarStyle: TitleBarStyle.hidden, ); if (props.left != null || props.top != null) { diff --git a/lib/controller.dart b/lib/controller.dart index 6f7ca6c4..8eb1c7ca 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -90,7 +90,7 @@ class AppController { final updateId = config.profiles.first.id; changeProfile(updateId); } else { - changeProfile(null); + updateSystemProxy(false); } } } @@ -193,6 +193,7 @@ class AppController { } handleBackOrExit() async { + print(config.isMinimizeOnExit); if (config.isMinimizeOnExit) { if (system.isDesktop) { await savePreferences(); @@ -410,8 +411,7 @@ class AppController { addProfileFormURL(url); } - int get columns => - other.getColumns(appState.viewMode, config.proxiesColumns); + int get columns => other.getColumns(appState.viewMode, config.proxiesColumns); updateViewWidth(double width) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -453,4 +453,9 @@ class AppController { ProxiesSortType.name => _sortOfName(proxies), }; } + + String getCurrentSelectedName(String groupName) { + final group = appState.getGroupWithName(groupName); + return config.currentSelectedMap[groupName] ?? group?.now ?? ''; + } } diff --git a/lib/fragments/about.dart b/lib/fragments/about.dart index 611594b7..ec629550 100644 --- a/lib/fragments/about.dart +++ b/lib/fragments/about.dart @@ -2,7 +2,6 @@ import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/list.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; @immutable class Contributor { @@ -90,7 +89,7 @@ class AboutFragment extends StatelessWidget { ]; return generateSection( separated: false, - title: appLocalizations.contributors, + title: appLocalizations.otherContributors, items: [ ListItem( title: SingleChildScrollView( diff --git a/lib/fragments/config.dart b/lib/fragments/config.dart index d5f903bc..14f6f784 100644 --- a/lib/fragments/config.dart +++ b/lib/fragments/config.dart @@ -141,7 +141,7 @@ class _ConfigFragmentState extends State { return generateSection( title: appLocalizations.app, items: [ - if (Platform.isAndroid) + if (Platform.isAndroid)...[ Selector( selector: (_, config) => config.allowBypass, builder: (_, allowBypass, __) { @@ -159,7 +159,6 @@ class _ConfigFragmentState extends State { ); }, ), - if (Platform.isAndroid) Selector( selector: (_, config) => config.systemProxy, builder: (_, systemProxy, __) { @@ -177,6 +176,24 @@ class _ConfigFragmentState extends State { ); }, ), + ], + Selector( + selector: (_, config) => config.isCloseConnections, + builder: (_, isCloseConnections, __) { + return ListItem.switchItem( + leading: const Icon(Icons.auto_delete_outlined), + title: Text(appLocalizations.autoCloseConnections), + subtitle: Text(appLocalizations.autoCloseConnectionsDesc), + delegate: SwitchDelegate( + value: isCloseConnections, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.config.isCloseConnections = value; + }, + ), + ); + }, + ), Selector( selector: (_, config) => config.isCompatible, builder: (_, isCompatible, __) { diff --git a/lib/fragments/connections.dart b/lib/fragments/connections.dart index 0de2f403..fbdbc990 100644 --- a/lib/fragments/connections.dart +++ b/lib/fragments/connections.dart @@ -37,8 +37,9 @@ class _ConnectionsFragmentState extends State { timer = Timer.periodic( const Duration(seconds: 1), (timer) { - connectionsNotifier.value = connectionsNotifier.value - .copyWith(connections: clashCore.getConnections()); + connectionsNotifier.value = connectionsNotifier.value.copyWith( + connections: clashCore.getConnections(), + ); }, ); }); @@ -50,6 +51,18 @@ class _ConnectionsFragmentState extends State { final commonScaffoldState = context.findAncestorStateOfType(); commonScaffoldState?.actions = [ + IconButton( + onPressed: () { + clashCore.closeConnections(); + connectionsNotifier.value = connectionsNotifier.value.copyWith( + connections: clashCore.getConnections(), + ); + }, + icon: const Icon(Icons.delete_sweep_outlined), + ), + const SizedBox( + width: 8, + ), IconButton( onPressed: () { showSearch( @@ -87,7 +100,7 @@ class _ConnectionsFragmentState extends State { } _handleBlockConnection(String id) { - clashCore.closeConnections(id); + clashCore.closeConnection(id); connectionsNotifier.value = connectionsNotifier.value .copyWith(connections: clashCore.getConnections()); } @@ -227,7 +240,7 @@ class ConnectionsSearchDelegate extends SearchDelegate { } _handleBlockConnection(String id) { - clashCore.closeConnections(id); + clashCore.closeConnection(id); connectionsNotifier.value = connectionsNotifier.value.copyWith( connections: clashCore.getConnections(), ); diff --git a/lib/fragments/dashboard/dashboard.dart b/lib/fragments/dashboard/dashboard.dart index 0e3fe22a..610335f8 100644 --- a/lib/fragments/dashboard/dashboard.dart +++ b/lib/fragments/dashboard/dashboard.dart @@ -35,7 +35,7 @@ class _DashboardFragmentState extends State { // final viewMode = other.getViewMode(viewWidth); // final isDesktop = viewMode == ViewMode.desktop; return Grid( - crossAxisCount: max(4 * ((viewWidth / 320).ceil()), 8), + crossAxisCount: max(4 * ((viewWidth / 350).ceil()), 8), crossAxisSpacing: 16, mainAxisSpacing: 16, children: const [ diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index f861465d..e9f0b643 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -313,13 +313,6 @@ class _ProfileItemState extends State { ), Row( children: [ - Text( - appLocalizations.expirationTime, - style: textTheme.labelMedium?.toLighter, - ), - const SizedBox( - width: 4, - ), Text( expireShow, style: textTheme.labelMedium?.toLighter, diff --git a/lib/fragments/proxies/card.dart b/lib/fragments/proxies/card.dart index 9f353570..d58971d2 100644 --- a/lib/fragments/proxies/card.dart +++ b/lib/fragments/proxies/card.dart @@ -105,11 +105,10 @@ class ProxyCard extends StatelessWidget { groupName, proxy.name, ); - clashCore.changeProxy( - ChangeProxyParams( - groupName: groupName, - proxyName: proxy.name, - ), + globalState.changeProxy( + config: appController.config, + groupName: groupName, + proxyName: proxy.name, ); } diff --git a/lib/fragments/proxies/common.dart b/lib/fragments/proxies/common.dart new file mode 100644 index 00000000..31453b5a --- /dev/null +++ b/lib/fragments/proxies/common.dart @@ -0,0 +1,75 @@ +import 'dart:math'; + +import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/common/constant.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +Widget currentProxyNameBuilder({ + required String groupName, + required Widget Function(String) builder, +}) { + return Selector2( + selector: (_, appState, config) { + final group = appState.getGroupWithName(groupName); + return config.currentSelectedMap[groupName] ?? group?.now ?? ''; + }, + builder: (_, value, ___) { + return builder(value); + }, + ); +} + +double get listHeaderHeight { + final measure = globalState.appController.measure; + return 24 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight; +} + +double getItemHeight(ProxyCardType proxyCardType) { + final measure = globalState.appController.measure; + final baseHeight = + 12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8; + return switch (proxyCardType) { + ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8, + ProxyCardType.shrink => baseHeight, + ProxyCardType.min => baseHeight - measure.bodyMediumHeight, + }; +} + +delayTest(List proxies) async { + final appController = globalState.appController; + for (final proxy in proxies) { + final proxyName = + appController.appState.getRealProxyName(proxy.name) ?? proxy.name; + globalState.appController.setDelay( + Delay( + name: proxyName, + value: 0, + ), + ); + clashCore.getDelay(proxyName).then((delay) { + globalState.appController.setDelay(delay); + }); + } + await Future.delayed(httpTimeoutDuration + moreDuration); + appController.appState.sortNum++; +} + +double getScrollToSelectedOffset({ + required String groupName, + required List proxies, +}) { + final appController = globalState.appController; + final columns = appController.columns; + final proxyCardType = appController.config.proxyCardType; + final selectedName = appController.getCurrentSelectedName(groupName); + final findSelectedIndex = proxies.indexWhere( + (proxy) => proxy.name == selectedName, + ); + final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0; + final rows = ((selectedIndex - 1) / columns).ceil(); + return max(rows * (getItemHeight(proxyCardType) + 8) - 8, 0); +} diff --git a/lib/fragments/proxies/group.dart b/lib/fragments/proxies/group.dart deleted file mode 100644 index eacc41ee..00000000 --- a/lib/fragments/proxies/group.dart +++ /dev/null @@ -1,404 +0,0 @@ -import 'dart:math'; - -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'; - -import 'card.dart'; - -class ProxyGroupView extends StatefulWidget { - final String groupName; - final ProxiesType type; - - const ProxyGroupView({ - super.key, - required this.groupName, - required this.type, - }); - - @override - State createState() => _ProxyGroupViewState(); -} - -class _ProxyGroupViewState extends State { - var isLock = false; - final scrollController = ScrollController(); - var isEnd = false; - - String get groupName => widget.groupName; - - ProxiesType get type => widget.type; - - double _getItemHeight(ProxyCardType proxyCardType) { - final measure = globalState.appController.measure; - final baseHeight = - 12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8; - return switch(proxyCardType){ - ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8, - ProxyCardType.shrink => baseHeight, - ProxyCardType.min => baseHeight - measure.bodyMediumHeight, - }; - } - - _delayTest(List proxies) async { - if (isLock) return; - isLock = true; - final appController = globalState.appController; - for (final proxy in proxies) { - final proxyName = - appController.appState.getRealProxyName(proxy.name) ?? proxy.name; - globalState.appController.setDelay( - Delay( - name: proxyName, - value: 0, - ), - ); - clashCore.getDelay(proxyName).then((delay) { - globalState.appController.setDelay(delay); - }); - } - await Future.delayed(httpTimeoutDuration + moreDuration); - appController.appState.sortNum++; - isLock = false; - } - - Widget _currentProxyNameBuilder({ - required Widget Function(String) builder, - }) { - return Selector2( - selector: (_, appState, config) { - final group = appState.getGroupWithName(groupName)!; - return config.currentSelectedMap[groupName] ?? group.now ?? ''; - }, - builder: (_, value, ___) { - return builder(value); - }, - ); - } - - Widget _buildTabGroupView({ - required List proxies, - required int columns, - required ProxyCardType proxyCardType, - }) { - final sortedProxies = globalState.appController.getSortProxies( - proxies, - ); - return DelayTestButtonContainer( - onClick: () async { - await _delayTest( - proxies, - ); - }, - child: Align( - alignment: Alignment.topCenter, - child: GridView.builder( - padding: const EdgeInsets.all(16), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - mainAxisExtent: _getItemHeight(proxyCardType), - ), - itemCount: sortedProxies.length, - itemBuilder: (_, index) { - final proxy = sortedProxies[index]; - return _currentProxyNameBuilder(builder: (value) { - return ProxyCard( - type: proxyCardType, - key: ValueKey('$groupName.${proxy.name}'), - isSelected: value == proxy.name, - proxy: proxy, - groupName: groupName, - ); - }); - }, - ), - ), - ); - } - - Widget _buildExpansionGroupView({ - required List proxies, - required int columns, - required ProxyCardType proxyCardType, - }) { - final sortedProxies = globalState.appController.getSortProxies( - proxies, - ); - final group = - globalState.appController.appState.getGroupWithName(groupName)!; - final itemHeight = _getItemHeight(proxyCardType); - final innerHeight = context.appSize.height - 200; - final lines = (sortedProxies.length / columns).ceil(); - final minLines = - innerHeight >= 200 ? (innerHeight / itemHeight).floor() : 3; - final height = (itemHeight + 8) * min(lines, minLines) - 8; - return Selector>( - selector: (_, config) => config.currentUnfoldSet, - builder: (_, currentUnfoldSet, __) { - return CommonCard( - child: ExpansionTile( - childrenPadding: const EdgeInsets.all(8), - initiallyExpanded: currentUnfoldSet.contains(groupName), - iconColor: context.colorScheme.onSurfaceVariant, - onExpansionChanged: (value) { - final tempUnfoldSet = Set.from(currentUnfoldSet); - if (value) { - tempUnfoldSet.add(groupName); - } else { - tempUnfoldSet.remove(groupName); - } - globalState.appController.config.updateCurrentUnfoldSet( - tempUnfoldSet, - ); - }, - controlAffinity: ListTileControlAffinity.trailing, - title: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - flex: 1, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(groupName), - const SizedBox( - height: 4, - ), - Flexible( - flex: 1, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - group.type.name, - style: context.textTheme.labelMedium?.toLight, - ), - Flexible( - flex: 1, - child: _currentProxyNameBuilder( - builder: (value) { - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - if (value.isNotEmpty) ...[ - Icon( - Icons.arrow_right, - color: context - .colorScheme.onSurfaceVariant, - ), - Flexible( - flex: 1, - child: Text( - overflow: TextOverflow.ellipsis, - value, - style: context - .textTheme.labelMedium?.toLight, - ), - ), - ] - ], - ); - }, - ), - ), - ], - ), - ), - const SizedBox( - height: 4, - ), - ], - ), - ), - IconButton( - icon: Icon( - Icons.network_ping, - size: 20, - color: context.colorScheme.onSurfaceVariant, - ), - onPressed: () { - _delayTest(sortedProxies); - }, - ), - ], - ), - shape: const RoundedRectangleBorder( - side: BorderSide.none, - ), - collapsedShape: const RoundedRectangleBorder( - side: BorderSide.none, - ), - children: [ - SizedBox( - height: height, - child: GridView.builder( - key: widget.key, - controller: scrollController, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - mainAxisExtent: _getItemHeight(proxyCardType), - ), - itemCount: sortedProxies.length, - itemBuilder: (_, index) { - final proxy = sortedProxies[index]; - return _currentProxyNameBuilder( - builder: (value) { - return ProxyCard( - style: CommonCardType.filled, - type: proxyCardType, - isSelected: value == proxy.name, - key: ValueKey('$groupName.${proxy.name}'), - proxy: proxy, - groupName: groupName, - ); - }, - ); - }, - ), - ), - ], - ), - ); - }, - ); - } - - @override - void dispose() { - super.dispose(); - scrollController.dispose(); - } - - @override - Widget build(BuildContext context) { - return Selector2( - selector: (_, appState, config) { - final group = appState.getGroupWithName(groupName)!; - return ProxyGroupSelectorState( - proxyCardType: config.proxyCardType, - proxiesSortType: config.proxiesSortType, - columns: globalState.appController.columns, - sortNum: appState.sortNum, - proxies: group.all, - ); - }, - builder: (_, state, __) { - final proxies = state.proxies; - final columns = state.columns; - final proxyCardType = state.proxyCardType; - return switch (type) { - ProxiesType.tab => _buildTabGroupView( - proxies: proxies, - columns: columns, - proxyCardType: proxyCardType, - ), - ProxiesType.list => _buildExpansionGroupView( - proxies: proxies, - columns: columns, - proxyCardType: proxyCardType, - ), - }; - }, - ); - } -} - -class DelayTestButtonContainer extends StatefulWidget { - final Widget child; - final Future Function() onClick; - - const DelayTestButtonContainer({ - super.key, - required this.child, - required this.onClick, - }); - - @override - State createState() => - _DelayTestButtonContainerState(); -} - -class _DelayTestButtonContainerState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _scale; - - _healthcheck() async { - _controller.forward(); - await widget.onClick(); - _controller.reverse(); - } - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration( - milliseconds: 200, - ), - ); - _scale = Tween( - begin: 1.0, - end: 0.0, - ).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval( - 0, - 1, - ), - ), - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - _controller.reverse(); - return FloatLayout( - floatingWidget: FloatWrapper( - child: AnimatedBuilder( - animation: _controller.view, - builder: (_, child) { - return SizedBox( - width: 56, - height: 56, - child: Transform.scale( - scale: _scale.value, - child: child, - ), - ); - }, - child: FloatingActionButton( - heroTag: null, - onPressed: _healthcheck, - child: const Icon(Icons.network_ping), - ), - ), - ), - child: widget.child, - ); - } -} \ No newline at end of file diff --git a/lib/fragments/proxies/list.dart b/lib/fragments/proxies/list.dart index a4cbb2c3..1ce6955c 100644 --- a/lib/fragments/proxies/list.dart +++ b/lib/fragments/proxies/list.dart @@ -1,50 +1,512 @@ +import 'package:collection/collection.dart'; +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/card.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'group.dart'; +import 'card.dart'; +import 'common.dart'; + +typedef GroupNameProxiesMap = Map>; class ProxiesListFragment extends StatefulWidget { const ProxiesListFragment({super.key}); @override - State createState() => - _ProxiesListFragmentState(); + State createState() => _ProxiesListFragmentState(); } -class _ProxiesListFragmentState - extends State { +class _ProxiesListFragmentState extends State { + final _controller = ScrollController(); + final _headerStateNotifier = ValueNotifier( + const ProxiesListHeaderSelectorState( + offset: 0, + currentIndex: 0, + ), + ); + List _headerOffset = []; + GroupNameProxiesMap _lastGroupNameProxiesMap = {}; + + @override + void initState() { + super.initState(); + _controller.addListener(_adjustHeader); + } + + _adjustHeader() { + final offset = _controller.offset; + final index = _headerOffset.findInterval(offset); + final currentIndex = index; + double headerOffset = 0.0; + if (index + 1 <= _headerOffset.length - 1) { + final endOffset = _headerOffset[index + 1]; + final startOffset = endOffset - listHeaderHeight - 8; + if (offset > startOffset && offset < endOffset) { + headerOffset = offset - startOffset; + } + } + _headerStateNotifier.value = _headerStateNotifier.value.copyWith( + currentIndex: currentIndex, + offset: headerOffset, + ); + } + + double _getListItemHeight(Type type, ProxyCardType proxyCardType) { + return switch (type) { + const (SizedBox) => 8, + const (ListHeader) => listHeaderHeight, + Type() => getItemHeight(proxyCardType), + }; + } + + @override + void dispose() { + super.dispose(); + _headerStateNotifier.dispose(); + _controller.removeListener(_adjustHeader); + _controller.dispose(); + } + + _handleChange(Set currentUnfoldSet, String groupName) { + final tempUnfoldSet = Set.from(currentUnfoldSet); + if (tempUnfoldSet.contains(groupName)) { + tempUnfoldSet.remove(groupName); + } else { + tempUnfoldSet.add(groupName); + } + globalState.appController.config.updateCurrentUnfoldSet( + tempUnfoldSet, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + _adjustHeader(); + }); + } + + List _getItemHeightList( + List items, + ProxyCardType proxyCardType, + ) { + final itemHeightList = []; + List headerOffset = []; + double currentHeight = 0; + for (final item in items) { + if (item.runtimeType == ListHeader) { + headerOffset.add(currentHeight); + } + final itemHeight = _getListItemHeight(item.runtimeType, proxyCardType); + itemHeightList.add(itemHeight); + currentHeight = currentHeight + itemHeight; + } + _headerOffset = headerOffset; + return itemHeightList; + } + + List _buildItems({ + required List groupNames, + required int columns, + required Set currentUnfoldSet, + required ProxyCardType type, + }) { + final items = []; + final GroupNameProxiesMap groupNameProxiesMap = {}; + for (final groupName in groupNames) { + final group = + globalState.appController.appState.getGroupWithName(groupName)!; + final isExpand = currentUnfoldSet.contains(groupName); + items.addAll([ + ListHeader( + onScrollToSelected: _scrollToGroupSelected, + key: Key(groupName), + isExpand: isExpand, + group: group, + onChange: (String groupName) { + _handleChange(currentUnfoldSet, groupName); + }, + ), + const SizedBox( + height: 8, + ), + ]); + if (isExpand) { + final sortedProxies = globalState.appController.getSortProxies( + group.all, + ); + groupNameProxiesMap[groupName] = sortedProxies; + final chunks = sortedProxies.chunks(columns); + final rows = chunks.map((proxies) { + final children = proxies + .map( + (proxy) => Flexible( + child: currentProxyNameBuilder( + groupName: group.name, + builder: (currentProxyName) { + return ProxyCard( + type: type, + isSelected: currentProxyName == proxy.name, + key: ValueKey('$groupName.${proxy.name}'), + proxy: proxy, + groupName: groupName, + ); + }), + ), + ) + .fill( + columns, + filler: (_) => const Flexible( + child: SizedBox(), + ), + ) + .separated( + const SizedBox( + width: 8, + ), + ); + + return Row( + children: children.toList(), + ); + }).separated( + const SizedBox( + height: 8, + ), + ); + items.addAll( + [ + ...rows, + const SizedBox( + height: 8, + ), + ], + ); + } + } + _lastGroupNameProxiesMap = groupNameProxiesMap; + return items; + } + + _buildHeader({ + required String groupName, + required Set currentUnfoldSet, + }) { + final group = + globalState.appController.appState.getGroupWithName(groupName)!; + final isExpand = currentUnfoldSet.contains(groupName); + return SizedBox( + height: listHeaderHeight, + child: ListHeader( + onScrollToSelected: _scrollToGroupSelected, + key: Key(groupName), + isExpand: isExpand, + group: group, + onChange: (String groupName) { + _handleChange(currentUnfoldSet, groupName); + }, + ), + ); + } + + _scrollToGroupSelected(String groupName) { + final appController = globalState.appController; + final currentGroups = appController.appState.currentGroups; + final groupNames = currentGroups.map((e) => e.name).toList(); + final findIndex = groupNames.indexWhere((item) => item == groupName); + final index = findIndex != -1 ? findIndex : 0; + final currentInitOffset = _headerOffset[index]; + final proxies = _lastGroupNameProxiesMap[groupName]; + _controller.animateTo( + currentInitOffset + + getScrollToSelectedOffset( + groupName: groupName, + proxies: proxies ?? [], + ), + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } + @override Widget build(BuildContext context) { - return Selector2( + return Selector2( selector: (_, appState, config) { final currentGroups = appState.currentGroups; final groupNames = currentGroups.map((e) => e.name).toList(); - return ProxiesSelectorState( + return ProxiesListSelectorState( groupNames: groupNames, - currentGroupName: config.currentGroupName, + currentUnfoldSet: config.currentUnfoldSet, + proxyCardType: config.proxyCardType, + proxiesSortType: config.proxiesSortType, + columns: globalState.appController.columns, + sortNum: appState.sortNum, ); }, + shouldRebuild: (prev, next) { + if (!const ListEquality() + .equals(prev.groupNames, next.groupNames)) { + _headerStateNotifier.value = const ProxiesListHeaderSelectorState( + offset: 0, + currentIndex: 0, + ); + } + return prev != next; + }, builder: (_, state, __) { - return ListView.separated( - padding: const EdgeInsets.all(16), - itemCount: state.groupNames.length, - itemBuilder: (_, index) { - final groupName = state.groupNames[index]; - return ProxyGroupView( - key: PageStorageKey(groupName), - groupName: groupName, - type: ProxiesType.list, - ); - }, - separatorBuilder: (BuildContext context, int index) { - return const SizedBox( - height: 16, - ); - }, + final items = _buildItems( + groupNames: state.groupNames, + currentUnfoldSet: state.currentUnfoldSet, + columns: state.columns, + type: state.proxyCardType, + ); + final itemsOffset = _getItemHeightList(items, state.proxyCardType); + return Scrollbar( + controller: _controller, + thumbVisibility: true, + trackVisibility: true, + thickness: 8, + radius: const Radius.circular(8), + interactive: true, + child: Stack( + children: [ + Positioned.fill( + child: ScrollConfiguration( + behavior: HiddenBarScrollBehavior(), + child: ListView.builder( + padding: const EdgeInsets.all(16), + controller: _controller, + itemExtentBuilder: (index, __) { + return itemsOffset[index]; + }, + itemCount: items.length, + itemBuilder: (_, index) { + return items[index]; + }, + ), + ), + ), + LayoutBuilder(builder: (_, container) { + return ValueListenableBuilder( + valueListenable: _headerStateNotifier, + builder: (_, headerState, ___) { + final index = + headerState.currentIndex > state.groupNames.length - 1 + ? 0 + : headerState.currentIndex; + return Stack( + children: [ + Positioned( + top: -headerState.offset, + child: Container( + width: container.maxWidth, + color: context.colorScheme.surface, + padding: const EdgeInsets.only( + top: 16, + left: 16, + right: 16, + bottom: 8, + ), + child: _buildHeader( + groupName: state.groupNames[index], + currentUnfoldSet: state.currentUnfoldSet, + ), + ), + ), + ], + ); + }, + ); + }), + ], + ), ); }, ); } -} \ No newline at end of file +} + +class ListHeader extends StatefulWidget { + final Group group; + + final Function(String groupName) onChange; + final Function(String groupName) onScrollToSelected; + final bool isExpand; + + const ListHeader({ + super.key, + required this.group, + required this.onChange, + required this.onScrollToSelected, + required this.isExpand, + }); + + @override + State createState() => _ListHeaderState(); +} + +class _ListHeaderState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _iconTurns; + var isLock = false; + + String get groupName => widget.group.name; + + String get groupType => widget.group.type.name; + + bool get isExpand => widget.isExpand; + + _delayTest(List proxies) async { + if (isLock) return; + isLock = true; + await delayTest(proxies); + isLock = false; + } + + _handleChange(String groupName) { + if (isExpand) { + _animationController.reverse(); + } else { + _animationController.forward(); + } + widget.onChange(groupName); + } + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _iconTurns = _animationController.drive( + Tween(begin: 0.0, end: 0.5), + ); + if (isExpand) { + _animationController.value = 1.0; + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return CommonCard( + key: widget.key, + type: CommonCardType.filled, + child: Container( + padding: const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + groupName, + style: context.textTheme.titleMedium?.copyWith( + color: context.colorScheme.primary, + ), + ), + const SizedBox( + height: 4, + ), + Flexible( + flex: 1, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + groupType, + style: context.textTheme.labelMedium?.toLight, + ), + Flexible( + flex: 1, + child: currentProxyNameBuilder( + groupName: groupName, + builder: (value) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (value.isNotEmpty) ...[ + Flexible( + flex: 1, + child: Text( + overflow: TextOverflow.ellipsis, + " · $value", + style: context + .textTheme.labelMedium?.toLight, + ), + ), + ] + ], + ); + }, + ), + ), + ], + ), + ), + ], + ), + ), + Row( + children: [ + if (isExpand) ...[ + IconButton( + onPressed: () { + widget.onScrollToSelected(groupName); + }, + icon: const Icon( + Icons.adjust, + ), + ), + IconButton( + onPressed: () { + _delayTest(widget.group.all); + }, + icon: const Icon( + Icons.network_ping, + ), + ), + const SizedBox( + width: 4, + ), + ], + AnimatedBuilder( + animation: _animationController.view, + builder: (_, __) { + return IconButton.filledTonal( + onPressed: () { + _handleChange(groupName); + }, + icon: RotationTransition( + turns: _iconTurns, + child: const Icon( + Icons.expand_more, + ), + ), + ); + }, + ) + ], + ) + ], + ), + ), + onPressed: () { + _handleChange(groupName); + }, + ); + } +} diff --git a/lib/fragments/proxies/proxies.dart b/lib/fragments/proxies/proxies.dart index c70d6509..2ce2b28c 100644 --- a/lib/fragments/proxies/proxies.dart +++ b/lib/fragments/proxies/proxies.dart @@ -17,12 +17,26 @@ class ProxiesFragment extends StatefulWidget { } class _ProxiesFragmentState extends State { + final GlobalKey _proxiesTabKey = GlobalKey(); - _initActions() { + _initActions(ProxiesType proxiesType) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final commonScaffoldState = context.findAncestorStateOfType(); commonScaffoldState?.actions = [ + if (proxiesType == ProxiesType.tab) ...[ + IconButton( + onPressed: () { + _proxiesTabKey.currentState?.scrollToGroupSelected(); + }, + icon: const Icon( + Icons.gps_fixed, + ), + ), + const SizedBox( + width: 8, + ) + ], IconButton( onPressed: () { showSheet( @@ -43,23 +57,24 @@ class _ProxiesFragmentState extends State { @override Widget build(BuildContext context) { - return Selector( - selector: (_, appState) => appState.currentLabel == 'proxies', - builder: (_, isCurrent, child) { - if (isCurrent) { - _initActions(); - } - return child!; + return Selector( + selector: (_, config) => config.proxiesType, + builder: (_, proxiesType, __) { + return Selector( + selector: (_, appState) => appState.currentLabel == 'proxies', + builder: (_, isCurrent, child) { + if (isCurrent) { + _initActions(proxiesType); + } + return switch (proxiesType) { + ProxiesType.tab => ProxiesTabFragment( + key: _proxiesTabKey, + ), + ProxiesType.list => const ProxiesListFragment(), + }; + }, + ); }, - child: Selector( - selector: (_, config) => config.proxiesType, - builder: (_, proxiesType, __) { - return switch (proxiesType) { - ProxiesType.tab => const ProxiesTabFragment(), - ProxiesType.list => const ProxiesListFragment(), - }; - }, - ), ); } } diff --git a/lib/fragments/proxies/tab.dart b/lib/fragments/proxies/tab.dart index 3002347b..f9fd68ca 100644 --- a/lib/fragments/proxies/tab.dart +++ b/lib/fragments/proxies/tab.dart @@ -8,19 +8,23 @@ import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'group.dart'; +import 'card.dart'; +import 'common.dart'; + +typedef GroupNameKeyMap = Map>; class ProxiesTabFragment extends StatefulWidget { const ProxiesTabFragment({super.key}); @override - State createState() => _ProxiesTabFragmentState(); + State createState() => ProxiesTabFragmentState(); } -class _ProxiesTabFragmentState extends State +class ProxiesTabFragmentState extends State with TickerProviderStateMixin { TabController? _tabController; - final hasMoreButtonNotifier = ValueNotifier(false); + final _hasMoreButtonNotifier = ValueNotifier(false); + GroupNameKeyMap _keyMap = {}; @override void dispose() { @@ -28,6 +32,11 @@ class _ProxiesTabFragmentState extends State _tabController?.dispose(); } + scrollToGroupSelected() { + final currentGroupName = globalState.appController.config.currentGroupName; + _keyMap[currentGroupName]?.currentState?.scrollToSelected(); + } + _buildMoreButton() { return Selector( selector: (_, appState) => appState.viewMode == ViewMode.mobile, @@ -83,6 +92,7 @@ class _ProxiesTabFragmentState extends State .updateCurrentGroupName( groupName, ); + Navigator.of(context).pop(); }, isSelected: groupName == state.currentGroupName, ) @@ -126,18 +136,29 @@ class _ProxiesTabFragmentState extends State initialIndex: index == -1 ? 0 : index, vsync: this, ); + GroupNameKeyMap keyMap = {}; + final children = state.groupNames.map((groupName) { + keyMap[groupName] = GlobalObjectKey(groupName); + return KeepContainer( + child: ProxyGroupView( + key: keyMap[groupName], + groupName: groupName, + ), + ); + }).toList(); + _keyMap = keyMap; return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ NotificationListener( onNotification: (scrollNotification) { - hasMoreButtonNotifier.value = + _hasMoreButtonNotifier.value = scrollNotification.metrics.maxScrollExtent > 0; return true; }, child: ValueListenableBuilder( - valueListenable: hasMoreButtonNotifier, + valueListenable: _hasMoreButtonNotifier, builder: (_, value, child) { return Stack( alignment: AlignmentDirectional.centerStart, @@ -199,16 +220,7 @@ class _ProxiesTabFragmentState extends State Expanded( child: TabBarView( controller: _tabController, - children: [ - for (final groupName in state.groupNames) - KeepContainer( - key: ObjectKey(groupName), - child: ProxyGroupView( - groupName: groupName, - type: ProxiesType.tab, - ), - ), - ], + children: children, ), ) ], @@ -217,3 +229,205 @@ class _ProxiesTabFragmentState extends State ); } } + +class ProxyGroupView extends StatefulWidget { + final String groupName; + + const ProxyGroupView({ + super.key, + required this.groupName, + }); + + @override + State createState() => ProxyGroupViewState(); +} + +class ProxyGroupViewState extends State { + var isLock = false; + final _controller = ScrollController(); + List _lastProxies = []; + + String get groupName => widget.groupName; + + _delayTest(List proxies) async { + if (isLock) return; + isLock = true; + await delayTest(proxies); + isLock = false; + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } + + Widget _buildTabGroupView({ + required List proxies, + required int columns, + required ProxyCardType proxyCardType, + }) { + final sortedProxies = globalState.appController.getSortProxies( + proxies, + ); + _lastProxies = sortedProxies; + return DelayTestButtonContainer( + onClick: () async { + await _delayTest( + proxies, + ); + }, + child: Align( + alignment: Alignment.topCenter, + child: GridView.builder( + controller: _controller, + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + mainAxisExtent: getItemHeight(proxyCardType), + ), + itemCount: sortedProxies.length, + itemBuilder: (_, index) { + final proxy = sortedProxies[index]; + return currentProxyNameBuilder( + builder: (value) { + return ProxyCard( + type: proxyCardType, + key: ValueKey('$groupName.${proxy.name}'), + isSelected: value == proxy.name, + proxy: proxy, + groupName: groupName, + ); + }, + groupName: groupName, + ); + }, + ), + ), + ); + } + + scrollToSelected() { + _controller.animateTo( + 16 + + getScrollToSelectedOffset( + groupName: groupName, + proxies: _lastProxies, + ), + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } + + @override + Widget build(BuildContext context) { + return Selector2( + selector: (_, appState, config) { + final group = appState.getGroupWithName(groupName)!; + return ProxyGroupSelectorState( + proxyCardType: config.proxyCardType, + proxiesSortType: config.proxiesSortType, + columns: globalState.appController.columns, + sortNum: appState.sortNum, + proxies: group.all, + ); + }, + builder: (_, state, __) { + final proxies = state.proxies; + final columns = state.columns; + final proxyCardType = state.proxyCardType; + return _buildTabGroupView( + proxies: proxies, + columns: columns, + proxyCardType: proxyCardType, + ); + }, + ); + } +} + +class DelayTestButtonContainer extends StatefulWidget { + final Widget child; + final Future Function() onClick; + + const DelayTestButtonContainer({ + super.key, + required this.child, + required this.onClick, + }); + + @override + State createState() => + _DelayTestButtonContainerState(); +} + +class _DelayTestButtonContainerState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scale; + + _healthcheck() async { + _controller.forward(); + await widget.onClick(); + _controller.reverse(); + } + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration( + milliseconds: 200, + ), + ); + _scale = Tween( + begin: 1.0, + end: 0.0, + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval( + 0, + 1, + ), + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + _controller.reverse(); + return FloatLayout( + floatingWidget: FloatWrapper( + child: AnimatedBuilder( + animation: _controller.view, + builder: (_, child) { + return SizedBox( + width: 56, + height: 56, + child: Transform.scale( + scale: _scale.value, + child: child, + ), + ); + }, + child: FloatingActionButton( + heroTag: null, + onPressed: _healthcheck, + child: const Icon(Icons.network_ping), + ), + ), + ), + child: widget.child, + ); + } +} diff --git a/lib/fragments/theme.dart b/lib/fragments/theme.dart index 83480fd2..763c4def 100644 --- a/lib/fragments/theme.dart +++ b/lib/fragments/theme.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; @@ -23,27 +21,6 @@ class ThemeModeItem { class ThemeFragment extends StatelessWidget { const ThemeFragment({super.key}); - Widget _itemCard({ - required BuildContext context, - required Info info, - required Widget child, - }) { - return Padding( - padding: const EdgeInsets.only( - top: 16, - ), - child: Wrap( - runSpacing: 16, - children: [ - InfoHeader( - info: info, - ), - child, - ], - ), - ); - } - @override Widget build(BuildContext context) { final previewCard = Padding( diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 6d6e1b7e..ccadef46 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -214,5 +214,7 @@ "proxyGroup": "Proxy group", "go": "Go", "externalLink": "External link", - "contributors": "Contributors" + "otherContributors": "Other contributors", + "autoCloseConnections": "Auto lose connections", + "autoCloseConnectionsDesc": "Auto close connections after change node" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb index 41c987bb..65c600e1 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -214,5 +214,7 @@ "proxyGroup": "代理组", "go": "前往", "externalLink": "外部链接", - "contributors": "贡献者" + "otherContributors": "其他贡献者", + "autoCloseConnections": "自动关闭连接", + "autoCloseConnectionsDesc": "切换节点后自动关闭连接" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 8c651348..fbdc730d 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -58,6 +58,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Auto check updates"), "autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage( "Auto check for updates when the app starts"), + "autoCloseConnections": + MessageLookupByLibrary.simpleMessage("Auto lose connections"), + "autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage( + "Auto close connections after change node"), "autoLaunch": MessageLookupByLibrary.simpleMessage("AutoLaunch"), "autoLaunchDesc": MessageLookupByLibrary.simpleMessage( "Follow the system self startup"), @@ -97,7 +101,6 @@ class MessageLookup extends MessageLookupByLibrary { "connectionsDesc": MessageLookupByLibrary.simpleMessage( "View current connections data"), "connectivity": MessageLookupByLibrary.simpleMessage("Connectivity:"), - "contributors": MessageLookupByLibrary.simpleMessage("Contributors"), "copy": MessageLookupByLibrary.simpleMessage("Copy"), "core": MessageLookupByLibrary.simpleMessage("Core"), "coreInfo": MessageLookupByLibrary.simpleMessage("Core info"), @@ -206,6 +209,8 @@ class MessageLookup extends MessageLookupByLibrary { "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"), "oneColumn": MessageLookupByLibrary.simpleMessage("One column"), "other": MessageLookupByLibrary.simpleMessage("Other"), + "otherContributors": + MessageLookupByLibrary.simpleMessage("Other contributors"), "outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"), "override": MessageLookupByLibrary.simpleMessage("Override"), "overrideDesc": MessageLookupByLibrary.simpleMessage( diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 63eb8137..8190eb78 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -49,6 +49,9 @@ class MessageLookup extends MessageLookupByLibrary { "autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"), "autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"), + "autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"), + "autoCloseConnectionsDesc": + MessageLookupByLibrary.simpleMessage("切换节点后自动关闭连接"), "autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"), "autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"), "autoRun": MessageLookupByLibrary.simpleMessage("自动运行"), @@ -79,7 +82,6 @@ class MessageLookup extends MessageLookupByLibrary { "connections": MessageLookupByLibrary.simpleMessage("连接"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"), "connectivity": MessageLookupByLibrary.simpleMessage("连通性:"), - "contributors": MessageLookupByLibrary.simpleMessage("贡献者"), "copy": MessageLookupByLibrary.simpleMessage("复制"), "core": MessageLookupByLibrary.simpleMessage("内核"), "coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"), @@ -169,6 +171,7 @@ class MessageLookup extends MessageLookupByLibrary { "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"), "oneColumn": MessageLookupByLibrary.simpleMessage("一列"), "other": MessageLookupByLibrary.simpleMessage("其他"), + "otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"), "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "override": MessageLookupByLibrary.simpleMessage("覆写"), "overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 66eb0936..8637f273 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -2200,11 +2200,31 @@ class AppLocalizations { ); } - /// `Contributors` - String get contributors { + /// `Other contributors` + String get otherContributors { return Intl.message( - 'Contributors', - name: 'contributors', + 'Other contributors', + name: 'otherContributors', + desc: '', + args: [], + ); + } + + /// `Auto lose connections` + String get autoCloseConnections { + return Intl.message( + 'Auto lose connections', + name: 'autoCloseConnections', + desc: '', + args: [], + ); + } + + /// `Auto close connections after change node` + String get autoCloseConnectionsDesc { + return Intl.message( + 'Auto close connections after change node', + name: 'autoCloseConnectionsDesc', desc: '', args: [], ); diff --git a/lib/main.dart b/lib/main.dart index 346b85e1..ef0dbcb1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,6 +47,7 @@ Future main() async { Future vpnService() async { WidgetsFlutterBinding.ensureInitialized(); globalState.isVpnService = true; + globalState.packageInfo = await PackageInfo.fromPlatform(); final config = await preferences.getConfig() ?? Config(); final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); final appState = AppState( @@ -86,11 +87,10 @@ Future vpnService() async { final currentSelectedMap = config.currentSelectedMap; final proxyName = currentSelectedMap[groupName]; if (proxyName == null) return; - clashCore.changeProxy( - ChangeProxyParams( - groupName: groupName, - proxyName: proxyName, - ), + globalState.changeProxy( + config: config, + groupName: groupName, + proxyName: proxyName, ); }, ), @@ -119,7 +119,7 @@ Future vpnService() async { globalState.updateTraffic(); globalState.updateFunctionLists = [ - () { + () { globalState.updateTraffic(); } ]; @@ -137,8 +137,7 @@ class ServiceMessageHandler with ServiceMessageListener { required Function(Process process) onProcess, required Function(String runTime) onStarted, required Function(String groupName) onLoaded, - }) - : _onProtect = onProtect, + }) : _onProtect = onProtect, _onProcess = onProcess, _onStarted = onStarted, _onLoaded = onLoaded; diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index 4ad29a73..4d1b3bc2 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -275,7 +275,7 @@ class ClashConfig extends ChangeNotifier { } } - @JsonKey(name: "global-ua", defaultValue: null) + @JsonKey(name: "global-ua", includeFromJson: false, includeToJson: true) String get globalUa { if (_globalRealUa == null) { return globalState.packageInfo.ua; @@ -320,7 +320,6 @@ class ClashConfig extends ChangeNotifier { _geodataLoader = clashConfig._geodataLoader; _dns = clashConfig._dns; _rules = clashConfig._rules; - _globalRealUa = clashConfig.globalRealUa; } notifyListeners(); } diff --git a/lib/models/config.dart b/lib/models/config.dart index d489aae0..b097b379 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -73,6 +73,7 @@ class Config extends ChangeNotifier { bool _systemProxy; bool _isExclude; DAV? _dav; + bool _isCloseConnections; ProxiesType _proxiesType; ProxyCardType _proxyCardType; int _proxiesColumns; @@ -84,6 +85,7 @@ class Config extends ChangeNotifier { _autoLaunch = false, _silentLaunch = false, _autoRun = false, + _isCloseConnections = false, _themeMode = ThemeMode.system, _openLog = false, _isCompatible = true, @@ -405,6 +407,18 @@ class Config extends ChangeNotifier { } } + @JsonKey(defaultValue: false) + bool get isCloseConnections { + return _isCloseConnections; + } + + set isCloseConnections(bool value) { + if (_isCloseConnections != value) { + _isCloseConnections = value; + notifyListeners(); + } + } + @JsonKey( defaultValue: ProxiesType.tab, unknownEnumValue: ProxiesType.tab, @@ -482,6 +496,7 @@ class Config extends ChangeNotifier { } if (onlyProfiles) return; _currentProfileId = config._currentProfileId; + _isCloseConnections = config._isCloseConnections; _isCompatible = config._isCompatible; _autoLaunch = config._autoLaunch; _silentLaunch = config._silentLaunch; diff --git a/lib/models/ffi.dart b/lib/models/ffi.dart index 86fb1a50..bd0bcb91 100644 --- a/lib/models/ffi.dart +++ b/lib/models/ffi.dart @@ -1,8 +1,6 @@ // ignore_for_file: invalid_annotation_target import 'package:fl_clash/enum/enum.dart'; -import 'package:fl_clash/models/clash_config.dart'; -import 'package:fl_clash/models/connection.dart'; import 'package:fl_clash/models/models.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/lib/models/generated/clash_config.g.dart b/lib/models/generated/clash_config.g.dart index 01189e2b..006a934e 100644 --- a/lib/models/generated/clash_config.g.dart +++ b/lib/models/generated/clash_config.g.dart @@ -82,6 +82,7 @@ Map _$ClashConfigToJson(ClashConfig instance) => 'tun': instance.tun, 'dns': instance.dns, 'rules': instance.rules, + 'global-ua': instance.globalUa, 'global-real-ua': instance.globalRealUa, 'geox-url': instance.geoXUrl, }; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 597e8199..e626e541 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -34,7 +34,8 @@ Config _$ConfigFromJson(Map json) => Config() ..isCompatible = json['isCompatible'] as bool? ?? true ..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true ..allowBypass = json['allowBypass'] as bool? ?? true - ..systemProxy = json['systemProxy'] as bool? ?? true + ..systemProxy = json['systemProxy'] as bool? ?? false + ..isCloseConnections = json['isCloseConnections'] as bool? ?? false ..proxiesType = $enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType'], unknownValue: ProxiesType.tab) ?? ProxiesType.tab @@ -68,6 +69,7 @@ Map _$ConfigToJson(Config instance) => { 'autoCheckUpdate': instance.autoCheckUpdate, 'allowBypass': instance.allowBypass, 'systemProxy': instance.systemProxy, + 'isCloseConnections': instance.isCloseConnections, 'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!, 'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!, 'proxiesColumns': instance.proxiesColumns, diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index bb9133bd..bd40a83a 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -1756,6 +1756,257 @@ abstract class _ProxiesSelectorState implements ProxiesSelectorState { get copyWith => throw _privateConstructorUsedError; } +/// @nodoc +mixin _$ProxiesListSelectorState { + List get groupNames => throw _privateConstructorUsedError; + Set get currentUnfoldSet => throw _privateConstructorUsedError; + ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError; + ProxyCardType get proxyCardType => throw _privateConstructorUsedError; + num get sortNum => throw _privateConstructorUsedError; + int get columns => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ProxiesListSelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProxiesListSelectorStateCopyWith<$Res> { + factory $ProxiesListSelectorStateCopyWith(ProxiesListSelectorState value, + $Res Function(ProxiesListSelectorState) then) = + _$ProxiesListSelectorStateCopyWithImpl<$Res, ProxiesListSelectorState>; + @useResult + $Res call( + {List groupNames, + Set currentUnfoldSet, + ProxiesSortType proxiesSortType, + ProxyCardType proxyCardType, + num sortNum, + int columns}); +} + +/// @nodoc +class _$ProxiesListSelectorStateCopyWithImpl<$Res, + $Val extends ProxiesListSelectorState> + implements $ProxiesListSelectorStateCopyWith<$Res> { + _$ProxiesListSelectorStateCopyWithImpl(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? groupNames = null, + Object? currentUnfoldSet = null, + Object? proxiesSortType = null, + Object? proxyCardType = null, + Object? sortNum = null, + Object? columns = null, + }) { + return _then(_value.copyWith( + groupNames: null == groupNames + ? _value.groupNames + : groupNames // ignore: cast_nullable_to_non_nullable + as List, + currentUnfoldSet: null == currentUnfoldSet + ? _value.currentUnfoldSet + : currentUnfoldSet // ignore: cast_nullable_to_non_nullable + as Set, + proxiesSortType: null == proxiesSortType + ? _value.proxiesSortType + : proxiesSortType // ignore: cast_nullable_to_non_nullable + as ProxiesSortType, + proxyCardType: null == proxyCardType + ? _value.proxyCardType + : proxyCardType // ignore: cast_nullable_to_non_nullable + as ProxyCardType, + sortNum: null == sortNum + ? _value.sortNum + : sortNum // ignore: cast_nullable_to_non_nullable + as num, + columns: null == columns + ? _value.columns + : columns // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ProxiesListSelectorStateImplCopyWith<$Res> + implements $ProxiesListSelectorStateCopyWith<$Res> { + factory _$$ProxiesListSelectorStateImplCopyWith( + _$ProxiesListSelectorStateImpl value, + $Res Function(_$ProxiesListSelectorStateImpl) then) = + __$$ProxiesListSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List groupNames, + Set currentUnfoldSet, + ProxiesSortType proxiesSortType, + ProxyCardType proxyCardType, + num sortNum, + int columns}); +} + +/// @nodoc +class __$$ProxiesListSelectorStateImplCopyWithImpl<$Res> + extends _$ProxiesListSelectorStateCopyWithImpl<$Res, + _$ProxiesListSelectorStateImpl> + implements _$$ProxiesListSelectorStateImplCopyWith<$Res> { + __$$ProxiesListSelectorStateImplCopyWithImpl( + _$ProxiesListSelectorStateImpl _value, + $Res Function(_$ProxiesListSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? groupNames = null, + Object? currentUnfoldSet = null, + Object? proxiesSortType = null, + Object? proxyCardType = null, + Object? sortNum = null, + Object? columns = null, + }) { + return _then(_$ProxiesListSelectorStateImpl( + groupNames: null == groupNames + ? _value._groupNames + : groupNames // ignore: cast_nullable_to_non_nullable + as List, + currentUnfoldSet: null == currentUnfoldSet + ? _value._currentUnfoldSet + : currentUnfoldSet // ignore: cast_nullable_to_non_nullable + as Set, + proxiesSortType: null == proxiesSortType + ? _value.proxiesSortType + : proxiesSortType // ignore: cast_nullable_to_non_nullable + as ProxiesSortType, + proxyCardType: null == proxyCardType + ? _value.proxyCardType + : proxyCardType // ignore: cast_nullable_to_non_nullable + as ProxyCardType, + sortNum: null == sortNum + ? _value.sortNum + : sortNum // ignore: cast_nullable_to_non_nullable + as num, + columns: null == columns + ? _value.columns + : columns // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$ProxiesListSelectorStateImpl implements _ProxiesListSelectorState { + const _$ProxiesListSelectorStateImpl( + {required final List groupNames, + required final Set currentUnfoldSet, + required this.proxiesSortType, + required this.proxyCardType, + required this.sortNum, + required this.columns}) + : _groupNames = groupNames, + _currentUnfoldSet = currentUnfoldSet; + + final List _groupNames; + @override + List get groupNames { + if (_groupNames is EqualUnmodifiableListView) return _groupNames; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_groupNames); + } + + final Set _currentUnfoldSet; + @override + Set get currentUnfoldSet { + if (_currentUnfoldSet is EqualUnmodifiableSetView) return _currentUnfoldSet; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_currentUnfoldSet); + } + + @override + final ProxiesSortType proxiesSortType; + @override + final ProxyCardType proxyCardType; + @override + final num sortNum; + @override + final int columns; + + @override + String toString() { + return 'ProxiesListSelectorState(groupNames: $groupNames, currentUnfoldSet: $currentUnfoldSet, proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, columns: $columns)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProxiesListSelectorStateImpl && + const DeepCollectionEquality() + .equals(other._groupNames, _groupNames) && + const DeepCollectionEquality() + .equals(other._currentUnfoldSet, _currentUnfoldSet) && + (identical(other.proxiesSortType, proxiesSortType) || + other.proxiesSortType == proxiesSortType) && + (identical(other.proxyCardType, proxyCardType) || + other.proxyCardType == proxyCardType) && + (identical(other.sortNum, sortNum) || other.sortNum == sortNum) && + (identical(other.columns, columns) || other.columns == columns)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_groupNames), + const DeepCollectionEquality().hash(_currentUnfoldSet), + proxiesSortType, + proxyCardType, + sortNum, + columns); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ProxiesListSelectorStateImplCopyWith<_$ProxiesListSelectorStateImpl> + get copyWith => __$$ProxiesListSelectorStateImplCopyWithImpl< + _$ProxiesListSelectorStateImpl>(this, _$identity); +} + +abstract class _ProxiesListSelectorState implements ProxiesListSelectorState { + const factory _ProxiesListSelectorState( + {required final List groupNames, + required final Set currentUnfoldSet, + required final ProxiesSortType proxiesSortType, + required final ProxyCardType proxyCardType, + required final num sortNum, + required final int columns}) = _$ProxiesListSelectorStateImpl; + + @override + List get groupNames; + @override + Set get currentUnfoldSet; + @override + ProxiesSortType get proxiesSortType; + @override + ProxyCardType get proxyCardType; + @override + num get sortNum; + @override + int get columns; + @override + @JsonKey(ignore: true) + _$$ProxiesListSelectorStateImplCopyWith<_$ProxiesListSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + /// @nodoc mixin _$ProxyGroupSelectorState { ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError; @@ -2401,3 +2652,151 @@ abstract class _ColumnsSelectorState implements ColumnsSelectorState { _$$ColumnsSelectorStateImplCopyWith<_$ColumnsSelectorStateImpl> get copyWith => throw _privateConstructorUsedError; } + +/// @nodoc +mixin _$ProxiesListHeaderSelectorState { + double get offset => throw _privateConstructorUsedError; + int get currentIndex => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ProxiesListHeaderSelectorStateCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProxiesListHeaderSelectorStateCopyWith<$Res> { + factory $ProxiesListHeaderSelectorStateCopyWith( + ProxiesListHeaderSelectorState value, + $Res Function(ProxiesListHeaderSelectorState) then) = + _$ProxiesListHeaderSelectorStateCopyWithImpl<$Res, + ProxiesListHeaderSelectorState>; + @useResult + $Res call({double offset, int currentIndex}); +} + +/// @nodoc +class _$ProxiesListHeaderSelectorStateCopyWithImpl<$Res, + $Val extends ProxiesListHeaderSelectorState> + implements $ProxiesListHeaderSelectorStateCopyWith<$Res> { + _$ProxiesListHeaderSelectorStateCopyWithImpl(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? offset = null, + Object? currentIndex = null, + }) { + return _then(_value.copyWith( + offset: null == offset + ? _value.offset + : offset // ignore: cast_nullable_to_non_nullable + as double, + currentIndex: null == currentIndex + ? _value.currentIndex + : currentIndex // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ProxiesListHeaderSelectorStateImplCopyWith<$Res> + implements $ProxiesListHeaderSelectorStateCopyWith<$Res> { + factory _$$ProxiesListHeaderSelectorStateImplCopyWith( + _$ProxiesListHeaderSelectorStateImpl value, + $Res Function(_$ProxiesListHeaderSelectorStateImpl) then) = + __$$ProxiesListHeaderSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({double offset, int currentIndex}); +} + +/// @nodoc +class __$$ProxiesListHeaderSelectorStateImplCopyWithImpl<$Res> + extends _$ProxiesListHeaderSelectorStateCopyWithImpl<$Res, + _$ProxiesListHeaderSelectorStateImpl> + implements _$$ProxiesListHeaderSelectorStateImplCopyWith<$Res> { + __$$ProxiesListHeaderSelectorStateImplCopyWithImpl( + _$ProxiesListHeaderSelectorStateImpl _value, + $Res Function(_$ProxiesListHeaderSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? offset = null, + Object? currentIndex = null, + }) { + return _then(_$ProxiesListHeaderSelectorStateImpl( + offset: null == offset + ? _value.offset + : offset // ignore: cast_nullable_to_non_nullable + as double, + currentIndex: null == currentIndex + ? _value.currentIndex + : currentIndex // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$ProxiesListHeaderSelectorStateImpl + implements _ProxiesListHeaderSelectorState { + const _$ProxiesListHeaderSelectorStateImpl( + {required this.offset, required this.currentIndex}); + + @override + final double offset; + @override + final int currentIndex; + + @override + String toString() { + return 'ProxiesListHeaderSelectorState(offset: $offset, currentIndex: $currentIndex)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProxiesListHeaderSelectorStateImpl && + (identical(other.offset, offset) || other.offset == offset) && + (identical(other.currentIndex, currentIndex) || + other.currentIndex == currentIndex)); + } + + @override + int get hashCode => Object.hash(runtimeType, offset, currentIndex); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ProxiesListHeaderSelectorStateImplCopyWith< + _$ProxiesListHeaderSelectorStateImpl> + get copyWith => __$$ProxiesListHeaderSelectorStateImplCopyWithImpl< + _$ProxiesListHeaderSelectorStateImpl>(this, _$identity); +} + +abstract class _ProxiesListHeaderSelectorState + implements ProxiesListHeaderSelectorState { + const factory _ProxiesListHeaderSelectorState( + {required final double offset, + required final int currentIndex}) = _$ProxiesListHeaderSelectorStateImpl; + + @override + double get offset; + @override + int get currentIndex; + @override + @JsonKey(ignore: true) + _$$ProxiesListHeaderSelectorStateImplCopyWith< + _$ProxiesListHeaderSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/models/selector.dart b/lib/models/selector.dart index 63f94a28..83e38d07 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -99,6 +99,18 @@ class ProxiesSelectorState with _$ProxiesSelectorState { }) = _ProxiesSelectorState; } +@freezed +class ProxiesListSelectorState with _$ProxiesListSelectorState { + const factory ProxiesListSelectorState({ + required List groupNames, + required Set currentUnfoldSet, + required ProxiesSortType proxiesSortType, + required ProxyCardType proxyCardType, + required num sortNum, + required int columns, + }) = _ProxiesListSelectorState; +} + @freezed class ProxyGroupSelectorState with _$ProxyGroupSelectorState { const factory ProxyGroupSelectorState({ @@ -133,3 +145,11 @@ class ColumnsSelectorState with _$ColumnsSelectorState { required ViewMode viewMode, }) = _ColumnsSelectorState; } + +@freezed +class ProxiesListHeaderSelectorState with _$ProxiesListHeaderSelectorState { + const factory ProxiesListHeaderSelectorState({ + required double offset, + required int currentIndex, + }) = _ProxiesListHeaderSelectorState; +} \ No newline at end of file diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 0e658e3c..e86bf206 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -95,55 +95,57 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return LayoutBuilder( - builder: (_, container) { - final appController = globalState.appController; - final maxWidth = container.maxWidth; - if (appController.appState.viewWidth != maxWidth) { - globalState.appController.updateViewWidth(maxWidth); - } - return Selector2( - selector: (_, appState, config) { - return HomeSelectorState( - currentLabel: appState.currentLabel, - navigationItems: appState.currentNavigationItems, - viewMode: other.getViewMode(maxWidth), - locale: config.locale, - ); - }, - builder: (_, state, child) { - final viewMode = state.viewMode; - final navigationItems = state.navigationItems; - final currentLabel = state.currentLabel; - final index = navigationItems.lastIndexWhere( - (element) => element.label == currentLabel, - ); - final currentIndex = index == -1 ? 0 : index; - final navigationBar = _getNavigationBar( - context: context, - viewMode: viewMode, - navigationItems: navigationItems, - currentIndex: currentIndex, - ); - final bottomNavigationBar = - viewMode == ViewMode.mobile ? navigationBar : null; - final sideNavigationBar = - viewMode != ViewMode.mobile ? navigationBar : null; - return CommonScaffold( - key: globalState.homeScaffoldKey, - title: Intl.message( - currentLabel, - ), - sideNavigationBar: sideNavigationBar, - body: child!, - bottomNavigationBar: bottomNavigationBar, - ); - }, - child: const HomeBody( - key: Key("home_boy"), - ), - ); - }, + return PopContainer( + child: LayoutBuilder( + builder: (_, container) { + final appController = globalState.appController; + final maxWidth = container.maxWidth; + if (appController.appState.viewWidth != maxWidth) { + globalState.appController.updateViewWidth(maxWidth); + } + return Selector2( + selector: (_, appState, config) { + return HomeSelectorState( + currentLabel: appState.currentLabel, + navigationItems: appState.currentNavigationItems, + viewMode: other.getViewMode(maxWidth), + locale: config.locale, + ); + }, + builder: (_, state, child) { + final viewMode = state.viewMode; + final navigationItems = state.navigationItems; + final currentLabel = state.currentLabel; + final index = navigationItems.lastIndexWhere( + (element) => element.label == currentLabel, + ); + final currentIndex = index == -1 ? 0 : index; + final navigationBar = _getNavigationBar( + context: context, + viewMode: viewMode, + navigationItems: navigationItems, + currentIndex: currentIndex, + ); + final bottomNavigationBar = + viewMode == ViewMode.mobile ? navigationBar : null; + final sideNavigationBar = + viewMode != ViewMode.mobile ? navigationBar : null; + return CommonScaffold( + key: globalState.homeScaffoldKey, + title: Intl.message( + currentLabel, + ), + sideNavigationBar: sideNavigationBar, + body: child!, + bottomNavigationBar: bottomNavigationBar, + ); + }, + child: const HomeBody( + key: Key("home_boy"), + ), + ); + }, + ), ); } } diff --git a/lib/plugins/proxy.dart b/lib/plugins/proxy.dart index 1f871eb3..164e26f2 100644 --- a/lib/plugins/proxy.dart +++ b/lib/plugins/proxy.dart @@ -103,6 +103,7 @@ class Proxy extends ProxyPlatform { _handleServiceMessage(String message) { final m = ServiceMessage.fromJson(json.decode(message)); + debugPrint(m.toString()); switch (m.type) { case ServiceMessageType.protect: _serviceMessageHandler?.onProtect(Fd.fromJson(m.data)); diff --git a/lib/state.dart b/lib/state.dart index c77f5bfc..bb7265f4 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -53,7 +53,7 @@ class GlobalState { config: clashConfig, params: ConfigExtendedParams( isPatch: isPatch, - isCompatible: config.isCompatible, + isCompatible: true, selectedMap: config.currentSelectedMap, testUrl: config.testUrl, ), @@ -184,6 +184,22 @@ class GlobalState { ); } + changeProxy({ + required Config config, + required String groupName, + required String proxyName, + }) { + clashCore.changeProxy( + ChangeProxyParams( + groupName: groupName, + proxyName: proxyName, + ), + ); + if(config.isCloseConnections){ + clashCore.closeConnections(); + } + } + Future showCommonDialog({ required Widget child, }) async { diff --git a/lib/widgets/android_container.dart b/lib/widgets/android_container.dart index 0111af9b..37592c32 100644 --- a/lib/widgets/android_container.dart +++ b/lib/widgets/android_container.dart @@ -19,6 +19,7 @@ class AndroidContainer extends StatefulWidget { class _AndroidContainerState extends State with WidgetsBindingObserver { + Widget _excludeContainer(Widget child) { return Selector( selector: (_, config) => config.isExclude, diff --git a/lib/widgets/clash_message_container.dart b/lib/widgets/clash_message_container.dart index cd366ecc..c2fae4b0 100644 --- a/lib/widgets/clash_message_container.dart +++ b/lib/widgets/clash_message_container.dart @@ -60,11 +60,10 @@ class _ClashMessageContainerState extends State final currentSelectedMap = appController.config.currentSelectedMap; final proxyName = currentSelectedMap[groupName]; if (proxyName == null) return; - clashCore.changeProxy( - ChangeProxyParams( - groupName: groupName, - proxyName: proxyName, - ), + globalState.changeProxy( + config: appController.config, + groupName: groupName, + proxyName: proxyName, ); appController.addCheckIpNumDebounce(); super.onLoaded(proxyName); diff --git a/lib/widgets/pop_container.dart b/lib/widgets/pop_container.dart index 7b4f3a4e..0ea357f4 100644 --- a/lib/widgets/pop_container.dart +++ b/lib/widgets/pop_container.dart @@ -19,7 +19,7 @@ class _PopContainerState extends State { if (Platform.isAndroid) { return PopScope( canPop: false, - onPopInvoked: (didPop) async { + onPopInvoked: (_) async { final canPop = Navigator.canPop(context); if (canPop) { Navigator.pop(context); diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index d01b987b..73b46a43 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -1,7 +1,6 @@ import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; class CommonScaffold extends StatefulWidget { final Widget body; diff --git a/pubspec.lock b/pubspec.lock index 46794629..6c35df56 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -317,10 +317,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+2" fixnum: dependency: transitive description: @@ -377,10 +377,10 @@ packages: dependency: "direct main" description: name: freezed_annotation - sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2 + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -401,10 +401,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" gtk: dependency: transitive description: @@ -417,10 +417,10 @@ packages: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -457,10 +457,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "4161e1f843d8480d2e9025ee22411778c3c9eb7e40076dcf2da23d8242b7b51c" + sha256: a26dc9a03fe042440c1e4be554fb0fceae2bf6d887d7467fc48c688fa4a81889 url: "https://pub.dev" source: hosted - version: "0.8.12+3" + version: "0.8.12+7" image_picker_for_web: dependency: transitive description: @@ -729,10 +729,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a + sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e" url: "https://pub.dev" source: hosted - version: "2.2.6" + version: "2.2.7" path_provider_foundation: dependency: transitive description: @@ -761,10 +761,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -904,10 +904,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shared_preferences_web: dependency: transitive description: @@ -1061,18 +1061,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf + sha256: "95d8027db36a0e52caf55680f91e33ea6aa12a3ce608c90b06f4e429a21067ac" url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.3.5" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: @@ -1109,10 +1109,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" vector_math: dependency: transitive description: @@ -1149,18 +1149,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078" + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276 + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" webdav_client: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2fe63789..03a6feb7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.47+202407222 +version: 0.8.48+202407251 environment: sdk: '>=3.1.0 <4.0.0' diff --git a/test/command_test.dart b/test/command_test.dart index 1f2ad4e0..a6aeabc6 100644 --- a/test/command_test.dart +++ b/test/command_test.dart @@ -1,10 +1,34 @@ // ignore_for_file: avoid_print -import 'package:fl_clash/common/common.dart'; + +import 'dart:io'; void main() async { + // 定义服务器将要监听的地址和端口 + final host = InternetAddress.anyIPv4; // 监听所有网络接口 + const port = 8080; // 使用 8080 端口 + + try { + // 创建服务器 + final server = await HttpServer.bind(host, port); + print('服务器正在监听 ${server.address.address}:${server.port}'); - print("https://pqjc.site:10000/test.ymal".isUrl); - print("abcd".isUrl); - print("http://10.31.1.221:8848/cfa.yaml".isUrl); + // 监听请求 + await for (HttpRequest request in server) { + handleRequest(request); + } + } catch (e) { + print('服务器错误: $e'); + } } +void handleRequest(HttpRequest request) { + print(request.headers); + // 处理请求 + request.response + ..statusCode = HttpStatus.ok + ..headers.contentType = ContentType.html + ..write('

Hello, Dart Server!

'); + + // 完成响应 + request.response.close(); +}