diff --git a/lib/application.dart b/lib/application.dart index 66dc11a..c83ed74 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -149,6 +149,7 @@ class ApplicationState extends State { builder: (lightDynamic, darkDynamic) { _updateSystemColorSchemes(lightDynamic, darkDynamic); return MaterialApp( + debugShowCheckedModeBanner: false, navigatorKey: globalState.navigatorKey, localizationsDelegates: const [ AppLocalizations.delegate, @@ -156,6 +157,7 @@ class ApplicationState extends State { GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate ], + scrollBehavior: BaseScrollBehavior(), title: appName, locale: other.getLocaleForString(state.locale), supportedLocales: diff --git a/lib/common/common.dart b/lib/common/common.dart index 0e47945..4db663c 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -24,4 +24,5 @@ export 'function.dart'; export 'package.dart'; export 'measure.dart'; export 'service.dart'; -export 'iterable.dart'; \ No newline at end of file +export 'iterable.dart'; +export 'scroll.dart'; \ No newline at end of file diff --git a/lib/common/constant.dart b/lib/common/constant.dart index e662b8a..18798b2 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/clash_config.dart'; import 'package:flutter/material.dart'; @@ -15,10 +16,14 @@ const asnFileName = "ASN.mmdb"; const geoIpFileName = "GeoIP.dat"; const geoSiteFileName = "GeoSite.dat"; const GeoXMap defaultGeoXMap = { - "mmdb":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", - "asn":"https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb", - "geoip":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat", - "geosite":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat" + "mmdb": + "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", + "asn": + "https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb", + "geoip": + "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat", + "geosite": + "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat" }; const profilesDirectoryName = "profiles"; const localhost = "127.0.0.1"; @@ -39,4 +44,10 @@ final filter = ImageFilter.blur( tileMode: TileMode.mirror, ); +const viewModeColumnsMap = { + ViewMode.mobile: [2, 1], + ViewMode.laptop: [3, 2], + ViewMode.desktop: [4, 3], +}; + const defaultPrimaryColor = Colors.brown; diff --git a/lib/common/other.dart b/lib/common/other.dart index 7701daf..536b249 100644 --- a/lib/common/other.dart +++ b/lib/common/other.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; +import 'package:fl_clash/common/app_localizations.dart'; import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:flutter/material.dart'; @@ -189,6 +190,24 @@ class Other { if (viewWidth <= maxLaptopWidth) return ViewMode.laptop; return ViewMode.desktop; } + + int getColumns(ViewMode viewMode, int currentColumns) { + final targetColumnsArray = viewModeColumnsMap[viewMode]!; + if (targetColumnsArray.contains(currentColumns)) { + return currentColumns; + } + return targetColumnsArray.first; + } + + String getColumnsTextForInt(int number){ + return switch(number){ + 1 => appLocalizations.oneColumn, + 2 => appLocalizations.twoColumns, + 3 => appLocalizations.threeColumns, + 4 => appLocalizations.fourColumns, + int() => throw UnimplementedError(), + }; + } } final other = Other(); diff --git a/lib/common/scroll.dart b/lib/common/scroll.dart new file mode 100644 index 0000000..a595620 --- /dev/null +++ b/lib/common/scroll.dart @@ -0,0 +1,16 @@ +import 'dart:ui'; + +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; + +class BaseScrollBehavior extends MaterialScrollBehavior { + @override + Set get dragDevices => { + PointerDeviceKind.touch, + PointerDeviceKind.stylus, + PointerDeviceKind.invertedStylus, + PointerDeviceKind.trackpad, + if (system.isDesktop) PointerDeviceKind.mouse, + PointerDeviceKind.unknown, + }; +} diff --git a/lib/common/window.dart b/lib/common/window.dart index 701e0fb..dff6507 100644 --- a/lib/common/window.dart +++ b/lib/common/window.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:fl_clash/models/config.dart'; import 'package:flutter/material.dart'; import 'package:window_manager/window_manager.dart'; import 'package:windows_single_instance/windows_single_instance.dart'; @@ -8,7 +9,7 @@ import 'protocol.dart'; import 'system.dart'; class Window { - init() async { + init(WindowProps props) async { if (Platform.isWindows) { await WindowsSingleInstance.ensureSingleInstance([], "FlClash"); protocol.register("clash"); @@ -16,11 +17,17 @@ class Window { protocol.register("flclash"); } await windowManager.ensureInitialized(); - WindowOptions windowOptions = const WindowOptions( - size: Size(1000, 600), - minimumSize: Size(400, 600), - center: true, + WindowOptions windowOptions = WindowOptions( + size: Size(props.width, props.height), + minimumSize: const Size(380, 600), ); + if (props.left != null || props.top != null) { + await windowManager.setPosition( + Offset(props.left ?? 0, props.top ?? 0), + ); + } else { + await windowManager.setAlignment(Alignment.center); + } await windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.setPreventClose(true); }); diff --git a/lib/controller.dart b/lib/controller.dart index 17af747..6f7ca6c 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -411,14 +411,7 @@ class AppController { } int get columns => - globalState.getColumns(appState.viewMode, config.proxiesColumns); - - changeColumns() { - config.proxiesColumns = globalState.getColumns( - appState.viewMode, - columns - 1, - ); - } + other.getColumns(appState.viewMode, config.proxiesColumns); updateViewWidth(double width) { WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index 22de9d7..d5b9e44 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -82,6 +82,6 @@ enum ChipType { action, delete } enum CommonCardType { plain, filled } -enum ProxiesType { tab, expansion } +enum ProxiesType { tab, list } -enum ProxyCardType { expand, shrink } +enum ProxyCardType { expand, shrink, min } diff --git a/lib/fragments/connections.dart b/lib/fragments/connections.dart index 771faa3..0de2f40 100644 --- a/lib/fragments/connections.dart +++ b/lib/fragments/connections.dart @@ -1,11 +1,9 @@ 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'; @@ -63,9 +61,6 @@ class _ConnectionsFragmentState extends State { }, icon: const Icon(Icons.search), ), - const SizedBox( - width: 8, - ) ]; }, ); @@ -162,7 +157,12 @@ class _ConnectionsFragmentState extends State { key: Key(connection.id), connection: connection, onClick: _addKeyword, - onBlock: _handleBlockConnection, + trailing: IconButton( + icon: const Icon(Icons.block), + onPressed: () { + _handleBlockConnection(connection.id); + }, + ), ); }, separatorBuilder: (BuildContext context, int index) { @@ -181,113 +181,6 @@ class _ConnectionsFragmentState extends State { } } -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 _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( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), - tileTitleAlignment: ListTileTitleAlignment.titleHeight, - leading: Platform.isAndroid - ? Container( - margin: const EdgeInsets.only(top: 4), - width: 48, - height: 48, - child: FutureBuilder( - future: _getPackageIcon(connection), - builder: (_, snapshot) { - if (!snapshot.hasData && snapshot.data == null) { - return Container(); - } else { - return Image( - image: snapshot.data!, - gaplessPlayback: true, - width: 48, - height: 48, - ); - } - }, - ), - ) - : null, - title: Text( - _getRequestText(connection.metadata), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8, - ), - Text( - _getSourceText(connection), - ), - const SizedBox( - height: 8, - ), - Wrap( - runSpacing: 6, - spacing: 6, - children: [ - for (final chain in connection.chains) - CommonChip( - label: chain, - onPressed: () { - if (onClick == null) return; - onClick!(chain); - }, - ), - ], - ), - ], - ), - trailing: IconButton( - icon: const Icon(Icons.block), - onPressed: () { - if (onBlock == null) return; - onBlock!(connection.id); - }, - ), - ); - } -} - class ConnectionsSearchDelegate extends SearchDelegate { ValueNotifier connectionsNotifier; @@ -333,11 +226,11 @@ class ConnectionsSearchDelegate extends SearchDelegate { ); } - _handleBlockConnection(String id) { clashCore.closeConnections(id); - connectionsNotifier.value = connectionsNotifier.value - .copyWith(connections: clashCore.getConnections()); + connectionsNotifier.value = connectionsNotifier.value.copyWith( + connections: clashCore.getConnections(), + ); } @override @@ -417,7 +310,12 @@ class ConnectionsSearchDelegate extends SearchDelegate { key: Key(connection.id), connection: connection, onClick: _addKeyword, - onBlock: _handleBlockConnection, + trailing: IconButton( + icon: const Icon(Icons.block), + onPressed: () { + _handleBlockConnection(connection.id); + }, + ), ); }, separatorBuilder: (BuildContext context, int index) { diff --git a/lib/fragments/fragments.dart b/lib/fragments/fragments.dart index a91b54e..37f213a 100644 --- a/lib/fragments/fragments.dart +++ b/lib/fragments/fragments.dart @@ -1,4 +1,4 @@ -export 'proxies.dart'; +export 'proxies/proxies.dart'; export 'dashboard/dashboard.dart'; export 'tools.dart'; export 'profiles/profiles.dart'; diff --git a/lib/fragments/logs.dart b/lib/fragments/logs.dart index bbe2d88..83e6509 100644 --- a/lib/fragments/logs.dart +++ b/lib/fragments/logs.dart @@ -72,9 +72,6 @@ class _LogsFragmentState extends State { }, icon: const Icon(Icons.search), ), - const SizedBox( - width: 8, - ) ]; }); } diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 7999709..f861465 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -72,9 +72,6 @@ class _ProfilesFragmentState extends State { }, icon: const Icon(Icons.sync), ), - const SizedBox( - width: 8, - ) ]; }, ); @@ -145,12 +142,8 @@ class _ProfilesFragmentState extends State { alignment: Alignment.topCenter, child: NotificationListener( onNotification: (scrollNotification) { - WidgetsBinding.instance.addPostFrameCallback( - (_) { - hasPadding.value = - scrollNotification.metrics.maxScrollExtent > 0; - }, - ); + hasPadding.value = + scrollNotification.metrics.maxScrollExtent > 0; return true; }, child: ValueListenableBuilder( @@ -161,7 +154,7 @@ class _ProfilesFragmentState extends State { left: 16, right: 16, top: 16, - bottom: 16 + (hasPadding ? 56 : 0), + bottom: 16 + (hasPadding ? 72 : 0), ), child: Grid( mainAxisSpacing: 16, @@ -272,11 +265,13 @@ class _ProfileItemState extends State { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - profile.label ?? profile.id, - style: textTheme.titleMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, + Flexible( + child: Text( + profile.label ?? profile.id, + style: textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), Text( profile.lastUpdateDate?.lastUpdateTimeDesc ?? '', diff --git a/lib/fragments/proxies.dart b/lib/fragments/proxies.dart index ce4abc0..e69de29 100644 --- a/lib/fragments/proxies.dart +++ b/lib/fragments/proxies.dart @@ -1,824 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:fl_clash/clash/core.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 ProxiesFragment extends StatefulWidget { - const ProxiesFragment({super.key}); - - @override - State createState() => _ProxiesFragmentState(); -} - -class _ProxiesFragmentState extends State { - _initActions() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final commonScaffoldState = - context.findAncestorStateOfType(); - final items = [ - CommonPopupMenuItem( - action: ProxiesSortType.none, - label: appLocalizations.defaultSort, - iconData: Icons.reorder, - ), - 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( - selector: (_, config) => config.proxiesType, - builder: (_, proxiesType, __) { - return IconButton( - icon: Icon( - switch (proxiesType) { - ProxiesType.tab => Icons.view_list, - ProxiesType.expansion => Icons.view_carousel, - }, - ), - onPressed: () { - final config = globalState.appController.config; - config.proxiesType = config.proxiesType == ProxiesType.tab - ? ProxiesType.expansion - : ProxiesType.tab; - }, - ); - }, - ), - IconButton( - icon: const Icon( - Icons.view_column, - ), - onPressed: () { - globalState.appController.changeColumns(); - }, - ), - IconButton( - icon: const Icon(Icons.transform_sharp), - onPressed: () { - final config = globalState.appController.config; - config.proxyCardType = config.proxyCardType == ProxyCardType.expand - ? ProxyCardType.shrink - : ProxyCardType.expand; - }, - ), - Selector( - selector: (_, config) => config.proxiesSortType, - builder: (_, proxiesSortType, __) { - return CommonPopupMenu.radio( - items: items, - icon: const Icon(Icons.sort_sharp), - onSelected: (value) { - final config = context.read(); - config.proxiesSortType = value; - }, - selectedValue: proxiesSortType, - ); - }, - ), - const SizedBox( - width: 8, - ) - ]; - }); - } - - @override - Widget build(BuildContext context) { - return Selector( - selector: (_, appState) => appState.currentLabel == 'proxies', - builder: (_, isCurrent, child) { - if (isCurrent) { - _initActions(); - } - return child!; - }, - child: Selector( - selector: (_, config) => config.proxiesType, - builder: (_, proxiesType, __) { - return switch (proxiesType) { - ProxiesType.tab => const ProxiesTabFragment(), - ProxiesType.expansion => const ProxiesExpansionPanelFragment(), - }; - }, - ), - ); - } -} - -class ProxiesTabFragment extends StatefulWidget { - const ProxiesTabFragment({super.key}); - - @override - State createState() => _ProxiesTabFragmentState(); -} - -class _ProxiesTabFragmentState extends State - with TickerProviderStateMixin { - TabController? _tabController; - - _handleTabControllerChange() { - final indexIsChanging = _tabController?.indexIsChanging ?? false; - if (indexIsChanging) return; - final index = _tabController?.index; - if (index == null) return; - final appController = globalState.appController; - final currentGroups = appController.appState.currentGroups; - if (currentGroups.length > index) { - appController.config.updateCurrentGroupName(currentGroups[index].name); - } - } - - @override - void dispose() { - super.dispose(); - _tabController?.dispose(); - } - - @override - Widget build(BuildContext context) { - return Selector2( - 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() - .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, - 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( - controller: _tabController, - children: [ - for (final groupName in state.groupNames) - KeepContainer( - key: ObjectKey(groupName), - child: ProxyGroupView( - groupName: groupName, - type: ProxiesType.tab, - ), - ), - ], - ), - ) - ], - ); - }, - ); - } -} - -class ProxiesExpansionPanelFragment extends StatefulWidget { - const ProxiesExpansionPanelFragment({super.key}); - - @override - State createState() => - _ProxiesExpansionPanelFragmentState(); -} - -class _ProxiesExpansionPanelFragmentState - extends State { - @override - Widget build(BuildContext context) { - return Selector2( - 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 ProxyGroupView( - key: PageStorageKey(groupName), - groupName: groupName, - type: ProxiesType.expansion, - ); - }, - separatorBuilder: (BuildContext context, int index) { - return const SizedBox( - height: 16, - ); - }, - ); - }, - ); - } -} - -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 isExpand = proxyCardType == ProxyCardType.expand; - final measure = globalState.appController.measure; - final baseHeight = - 12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8; - return isExpand ? baseHeight + measure.labelSmallHeight + 8 : baseHeight; - } - - _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.expansion => _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, - ); - } -} - -class ProxyCard extends StatelessWidget { - final String groupName; - final Proxy proxy; - final bool isSelected; - final CommonCardType style; - final ProxyCardType type; - - const ProxyCard({ - super.key, - required this.groupName, - required this.proxy, - required this.isSelected, - this.style = CommonCardType.plain, - required this.type, - }); - - Measure get measure => globalState.appController.measure; - - Widget _buildDelayText() { - return SizedBox( - height: measure.labelSmallHeight, - child: Selector( - selector: (context, appState) => appState.getDelay( - proxy.name, - ), - builder: (context, delay, __) { - return FadeBox( - child: Builder( - builder: (_) { - if (delay == null) { - return Container(); - } - if (delay == 0) { - return SizedBox( - height: measure.labelSmallHeight, - width: measure.labelSmallHeight, - child: const CircularProgressIndicator( - strokeWidth: 2, - ), - ); - } - return Text( - delay > 0 ? '$delay ms' : "Timeout", - style: context.textTheme.labelSmall?.copyWith( - overflow: TextOverflow.ellipsis, - color: other.getDelayColor( - delay, - ), - ), - ); - }, - ), - ); - }, - ), - ); - } - - Widget _buildProxyNameText(BuildContext context) { - return SizedBox( - height: measure.bodyMediumHeight * 2, - child: Text( - proxy.name, - maxLines: 2, - style: context.textTheme.bodyMedium?.copyWith( - overflow: TextOverflow.ellipsis, - ), - ), - ); - } - - _changeProxy(BuildContext context) { - final appController = globalState.appController; - final group = appController.appState.getGroupWithName(groupName)!; - if (group.type != GroupType.Selector) { - globalState.showSnackBar( - context, - message: appLocalizations.notSelectedTip, - ); - return; - } - globalState.appController.config.updateCurrentSelectedMap( - groupName, - proxy.name, - ); - clashCore.changeProxy( - ChangeProxyParams( - groupName: groupName, - proxyName: proxy.name, - ), - ); - } - - @override - Widget build(BuildContext context) { - final measure = globalState.appController.measure; - final delayText = _buildDelayText(); - final proxyNameText = _buildProxyNameText(context); - return CommonCard( - type: style, - key: key, - onPressed: () { - _changeProxy(context); - }, - isSelected: isSelected, - child: Container( - padding: const EdgeInsets.all(12), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - proxyNameText, - const SizedBox( - height: 8, - ), - if (type == ProxyCardType.expand) ...[ - SizedBox( - height: measure.bodySmallHeight, - child: Selector( - selector: (context, appState) => appState.getDesc( - proxy.type, - proxy.name, - ), - builder: (_, desc, __) { - return TooltipText( - text: Text( - desc, - style: context.textTheme.bodySmall?.copyWith( - overflow: TextOverflow.ellipsis, - color: context.textTheme.bodySmall?.color?.toLight(), - ), - ), - ); - }, - ), - ), - const SizedBox( - height: 8, - ), - delayText, - ] else - SizedBox( - height: measure.bodySmallHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flexible( - flex: 1, - child: TooltipText( - text: Text( - proxy.type, - style: context.textTheme.bodySmall?.copyWith( - overflow: TextOverflow.ellipsis, - color: - context.textTheme.bodySmall?.color?.toLight(), - ), - ), - ), - ), - delayText, - ], - ), - ) - ], - ), - ), - ); - } -} diff --git a/lib/fragments/proxies/card.dart b/lib/fragments/proxies/card.dart new file mode 100644 index 0000000..9f35357 --- /dev/null +++ b/lib/fragments/proxies/card.dart @@ -0,0 +1,192 @@ +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 ProxyCard extends StatelessWidget { + final String groupName; + final Proxy proxy; + final bool isSelected; + final CommonCardType style; + final ProxyCardType type; + + const ProxyCard({ + super.key, + required this.groupName, + required this.proxy, + required this.isSelected, + this.style = CommonCardType.plain, + required this.type, + }); + + Measure get measure => globalState.appController.measure; + + Widget _buildDelayText() { + return SizedBox( + height: measure.labelSmallHeight, + child: Selector( + selector: (context, appState) => appState.getDelay( + proxy.name, + ), + builder: (context, delay, __) { + return FadeBox( + child: Builder( + builder: (_) { + if (delay == null) { + return Container(); + } + if (delay == 0) { + return SizedBox( + height: measure.labelSmallHeight, + width: measure.labelSmallHeight, + child: const CircularProgressIndicator( + strokeWidth: 2, + ), + ); + } + return Text( + delay > 0 ? '$delay ms' : "Timeout", + style: context.textTheme.labelSmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: other.getDelayColor( + delay, + ), + ), + ); + }, + ), + ); + }, + ), + ); + } + + Widget _buildProxyNameText(BuildContext context) { + if (type == ProxyCardType.min) { + return SizedBox( + height: measure.bodyMediumHeight * 1, + child: Text( + proxy.name, + maxLines: 1, + style: context.textTheme.bodyMedium?.copyWith( + overflow: TextOverflow.ellipsis, + ), + ), + ); + } else { + return SizedBox( + height: measure.bodyMediumHeight * 2, + child: Text( + proxy.name, + maxLines: 2, + style: context.textTheme.bodyMedium?.copyWith( + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + } + + _changeProxy(BuildContext context) { + final appController = globalState.appController; + final group = appController.appState.getGroupWithName(groupName)!; + if (group.type != GroupType.Selector) { + globalState.showSnackBar( + context, + message: appLocalizations.notSelectedTip, + ); + return; + } + globalState.appController.config.updateCurrentSelectedMap( + groupName, + proxy.name, + ); + clashCore.changeProxy( + ChangeProxyParams( + groupName: groupName, + proxyName: proxy.name, + ), + ); + } + + @override + Widget build(BuildContext context) { + final measure = globalState.appController.measure; + final delayText = _buildDelayText(); + final proxyNameText = _buildProxyNameText(context); + return CommonCard( + type: style, + key: key, + onPressed: () { + _changeProxy(context); + }, + isSelected: isSelected, + child: Container( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + proxyNameText, + const SizedBox( + height: 8, + ), + if (type == ProxyCardType.expand) ...[ + SizedBox( + height: measure.bodySmallHeight, + child: Selector( + selector: (context, appState) => appState.getDesc( + proxy.type, + proxy.name, + ), + builder: (_, desc, __) { + return TooltipText( + text: Text( + desc, + style: context.textTheme.bodySmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: context.textTheme.bodySmall?.color?.toLight(), + ), + ), + ); + }, + ), + ), + const SizedBox( + height: 8, + ), + delayText, + ] else + SizedBox( + height: measure.bodySmallHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + flex: 1, + child: TooltipText( + text: Text( + proxy.type, + style: context.textTheme.bodySmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: + context.textTheme.bodySmall?.color?.toLight(), + ), + ), + ), + ), + delayText, + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/fragments/proxies/group.dart b/lib/fragments/proxies/group.dart new file mode 100644 index 0000000..eacc41e --- /dev/null +++ b/lib/fragments/proxies/group.dart @@ -0,0 +1,404 @@ +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 new file mode 100644 index 0000000..a4cbb2c --- /dev/null +++ b/lib/fragments/proxies/list.dart @@ -0,0 +1,50 @@ +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'group.dart'; + +class ProxiesListFragment extends StatefulWidget { + const ProxiesListFragment({super.key}); + + @override + State createState() => + _ProxiesListFragmentState(); +} + +class _ProxiesListFragmentState + extends State { + @override + Widget build(BuildContext context) { + return Selector2( + 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 ProxyGroupView( + key: PageStorageKey(groupName), + groupName: groupName, + type: ProxiesType.list, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const SizedBox( + height: 16, + ); + }, + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/fragments/proxies/proxies.dart b/lib/fragments/proxies/proxies.dart new file mode 100644 index 0000000..c70d650 --- /dev/null +++ b/lib/fragments/proxies/proxies.dart @@ -0,0 +1,65 @@ +import 'package:fl_clash/common/app_localizations.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/fragments/proxies/list.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'setting.dart'; +import 'tab.dart'; + +class ProxiesFragment extends StatefulWidget { + const ProxiesFragment({super.key}); + + @override + State createState() => _ProxiesFragmentState(); +} + +class _ProxiesFragmentState extends State { + + _initActions() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final commonScaffoldState = + context.findAncestorStateOfType(); + commonScaffoldState?.actions = [ + IconButton( + onPressed: () { + showSheet( + title: appLocalizations.proxiesSetting, + context: context, + builder: (context) { + return const ProxiesSettingWidget(); + }, + ); + }, + icon: const Icon( + Icons.tune, + ), + ) + ]; + }); + } + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, appState) => appState.currentLabel == 'proxies', + builder: (_, isCurrent, child) { + if (isCurrent) { + _initActions(); + } + return child!; + }, + 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/setting.dart b/lib/fragments/proxies/setting.dart new file mode 100644 index 0000000..c884455 --- /dev/null +++ b/lib/fragments/proxies/setting.dart @@ -0,0 +1,262 @@ +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:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class ProxiesSettingWidget extends StatelessWidget { + const ProxiesSettingWidget({super.key}); + + IconData _getIconWithProxiesType(ProxiesType type) { + return switch (type) { + ProxiesType.tab => Icons.view_carousel, + ProxiesType.list => Icons.view_list, + }; + } + + IconData _getIconWithProxiesSortType(ProxiesSortType type) { + return switch (type) { + ProxiesSortType.none => Icons.sort, + ProxiesSortType.delay => Icons.network_ping, + ProxiesSortType.name => Icons.sort_by_alpha, + }; + } + + String _getStringProxiesSortType(ProxiesSortType type) { + return switch (type) { + ProxiesSortType.none => appLocalizations.defaultText, + ProxiesSortType.delay => appLocalizations.delay, + ProxiesSortType.name => appLocalizations.name, + }; + } + + List _buildStyleSetting() { + return generateSection( + title: appLocalizations.style, + items: [ + SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + child: Selector( + selector: (_, config) => config.proxiesType, + builder: (_, proxiesType, __) { + final config = globalState.appController.config; + return Wrap( + spacing: 16, + children: [ + for (final item in ProxiesType.values) + SettingInfoCard( + Info( + label: Intl.message(item.name), + iconData: _getIconWithProxiesType(item), + ), + isSelected: proxiesType == item, + onPressed: () { + config.proxiesType = item; + }, + ) + ], + ); + }, + ), + ) + ], + ); + } + + List _buildSortSetting() { + return generateSection( + title: appLocalizations.sort, + items: [ + SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + child: Selector( + selector: (_, config) => config.proxiesSortType, + builder: (_, proxiesSortType, __) { + final config = globalState.appController.config; + return Wrap( + spacing: 16, + children: [ + for (final item in ProxiesSortType.values) + SettingInfoCard( + Info( + label: _getStringProxiesSortType(item), + iconData: _getIconWithProxiesSortType(item), + ), + isSelected: proxiesSortType == item, + onPressed: () { + config.proxiesSortType = item; + }, + ), + ], + ); + }, + ), + ), + ], + ); + } + + List _buildSizeSetting() { + return generateSection( + title: appLocalizations.size, + items: [ + SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + child: Selector( + selector: (_, config) => config.proxyCardType, + builder: (_, proxyCardType, __) { + final config = globalState.appController.config; + return Wrap( + spacing: 16, + children: [ + for (final item in ProxyCardType.values) + SettingTextCard( + Intl.message(item.name), + isSelected: item == proxyCardType, + onPressed: () { + config.proxyCardType = item; + }, + ) + ], + ); + }, + ), + ) + ], + ); + } + + List _buildColumnsSetting() { + return generateSection( + title: appLocalizations.columns, + items: [ + SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + scrollDirection: Axis.horizontal, + child: Selector2( + selector: (_, appState, config) => ColumnsSelectorState( + columns: config.proxiesColumns, + viewMode: appState.viewMode, + ), + builder: (_, state, __) { + final config = globalState.appController.config; + final targetColumnsArray = viewModeColumnsMap[state.viewMode]!; + final currentColumns = other.getColumns( + state.viewMode, + state.columns, + ); + return Wrap( + spacing: 16, + children: [ + for (final item in targetColumnsArray) + SettingTextCard( + other.getColumnsTextForInt(item), + isSelected: item == currentColumns, + onPressed: () { + config.proxiesColumns = item; + }, + ) + ], + ); + }, + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ..._buildStyleSetting(), + ..._buildSortSetting(), + ..._buildColumnsSetting(), + ..._buildSizeSetting(), + ], + ), + ); + } +} + +class SettingInfoCard extends StatelessWidget { + final Info info; + final bool? isSelected; + final VoidCallback onPressed; + + const SettingInfoCard( + this.info, { + super.key, + this.isSelected, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return CommonCard( + isSelected: isSelected, + onPressed: onPressed, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Flexible( + child: Icon(info.iconData), + ), + const SizedBox( + width: 8, + ), + Flexible( + child: Text( + info.label, + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + ), + ); + } +} + +class SettingTextCard extends StatelessWidget { + final String text; + final bool? isSelected; + final VoidCallback onPressed; + + const SettingTextCard( + this.text, { + super.key, + this.isSelected, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return CommonCard( + onPressed: onPressed, + isSelected: isSelected, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + text, + style: context.textTheme.bodyMedium, + ), + ), + ); + } +} diff --git a/lib/fragments/proxies/tab.dart b/lib/fragments/proxies/tab.dart new file mode 100644 index 0000000..d05e37a --- /dev/null +++ b/lib/fragments/proxies/tab.dart @@ -0,0 +1,218 @@ +import 'package:collection/collection.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/fragments/proxies/setting.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 'group.dart'; + +class ProxiesTabFragment extends StatefulWidget { + const ProxiesTabFragment({super.key}); + + @override + State createState() => _ProxiesTabFragmentState(); +} + +class _ProxiesTabFragmentState extends State + with TickerProviderStateMixin { + TabController? _tabController; + final hasMoreButtonNotifier = ValueNotifier(false); + + @override + void dispose() { + super.dispose(); + _tabController?.dispose(); + } + + _buildMoreButton() { + return Selector( + selector: (_, appState) => appState.viewMode == ViewMode.mobile, + builder: (_, value, ___) { + return IconButton( + onPressed: _showMoreMenu, + icon: value + ? const Icon( + Icons.expand_more, + ) + : const Icon( + Icons.chevron_right, + ), + ); + }, + ); + } + + _showMoreMenu() { + showSheet( + context: context, + width: 380, + isScrollControlled: false, + builder: (context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Selector2( + 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 SizedBox( + width: double.infinity, + child: Wrap( + runSpacing: 8, + spacing: 8, + children: [ + for (final groupName in state.groupNames) + SettingTextCard( + groupName, + onPressed: () { + final index = state.groupNames + .indexWhere((item) => item == groupName); + if (index == -1) return; + _tabController?.animateTo(index); + globalState.appController.config + .updateCurrentGroupName( + groupName, + ); + }, + isSelected: groupName == state.currentGroupName, + ) + ], + ), + ); + }, + ), + ); + }, + title: appLocalizations.proxyGroup, + ); + } + + @override + Widget build(BuildContext context) { + return Selector2( + 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() + .equals(prev.groupNames, next.groupNames)) { + _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, + ); + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NotificationListener( + onNotification: (scrollNotification) { + hasMoreButtonNotifier.value = + scrollNotification.metrics.maxScrollExtent > 0; + return true; + }, + child: ValueListenableBuilder( + valueListenable: hasMoreButtonNotifier, + builder: (_, value, child) { + return Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + TabBar( + controller: _tabController, + padding: EdgeInsets.only( + left: 16, + right: 16 + (value ? 16 : 0), + ), + onTap: (index) { + final appController = globalState.appController; + final currentGroups = + appController.appState.currentGroups; + if (currentGroups.length > index) { + appController.config.updateCurrentGroupName( + currentGroups[index].name, + ); + } + }, + dividerColor: Colors.transparent, + isScrollable: true, + tabAlignment: TabAlignment.start, + overlayColor: + const WidgetStatePropertyAll(Colors.transparent), + tabs: [ + for (final groupName in state.groupNames) + Tab( + text: groupName, + ), + ], + ), + if (value) + Positioned( + right: 0, + child: child!, + ), + ], + ); + }, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + context.colorScheme.surface.withOpacity(0.1), + context.colorScheme.surface, + ], + stops: const [ + 0.0, + 0.1 + ]), + ), + child: _buildMoreButton(), + ), + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + for (final groupName in state.groupNames) + KeepContainer( + key: ObjectKey(groupName), + child: ProxyGroupView( + groupName: groupName, + type: ProxiesType.tab, + ), + ), + ], + ), + ) + ], + ); + }, + ); + } +} diff --git a/lib/fragments/requests.dart b/lib/fragments/requests.dart index b80b707..fbdea1e 100644 --- a/lib/fragments/requests.dart +++ b/lib/fragments/requests.dart @@ -1,11 +1,9 @@ import 'dart:async'; -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'; @@ -68,9 +66,6 @@ class _RequestsFragmentState extends State { }, icon: const Icon(Icons.search), ), - const SizedBox( - width: 8, - ) ]; }, ); @@ -156,7 +151,7 @@ class _RequestsFragmentState extends State { controller: _scrollController, itemBuilder: (_, index) { final connection = connections[index]; - return RequestItem( + return ConnectionItem( key: Key(connection.id), connection: connection, onClick: _addKeyword, @@ -178,104 +173,6 @@ class _RequestsFragmentState extends State { } } -class RequestItem extends StatelessWidget { - final Connection connection; - final Function(String)? onClick; - - const RequestItem({ - super.key, - required this.connection, - this.onClick, - }); - - Future _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( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), - tileTitleAlignment: ListTileTitleAlignment.titleHeight, - leading: Platform.isAndroid - ? Container( - margin: const EdgeInsets.only(top: 4), - width: 48, - height: 48, - child: FutureBuilder( - future: _getPackageIcon(connection), - builder: (_, snapshot) { - if (!snapshot.hasData && snapshot.data == null) { - return Container(); - } else { - return Image( - image: snapshot.data!, - gaplessPlayback: true, - width: 48, - height: 48, - ); - } - }, - ), - ) - : null, - title: Text( - _getRequestText(connection.metadata), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8, - ), - Text( - _getSourceText(connection), - ), - const SizedBox( - height: 8, - ), - Wrap( - runSpacing: 6, - spacing: 6, - children: [ - for (final chain in connection.chains) - CommonChip( - label: chain, - onPressed: () { - if (onClick == null) return; - onClick!(chain); - }, - ), - ], - ), - ], - ), - ); - } -} - class RequestsSearchDelegate extends SearchDelegate { ValueNotifier requestsNotifier; @@ -394,7 +291,7 @@ class RequestsSearchDelegate extends SearchDelegate { child: ListView.separated( itemBuilder: (_, index) { final connection = _results[index]; - return RequestItem( + return ConnectionItem( key: Key(connection.id), connection: connection, onClick: (value) { diff --git a/lib/fragments/theme.dart b/lib/fragments/theme.dart index 62a11f5..83480fd 100644 --- a/lib/fragments/theme.dart +++ b/lib/fragments/theme.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; @@ -21,8 +23,95 @@ class ThemeModeItem { class ThemeFragment extends StatelessWidget { const ThemeFragment({super.key}); - Widget _themeModeCheckBox({ + 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( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CommonCard( + onPressed: (){ + + }, + info: Info( + label: appLocalizations.preview, + iconData: Icons.looks, + ), + child: Container( + height: 200, + ), + ), + ); + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + previewCard, + const ThemeColorsBox(), + ], + ), + ); + } +} + +class ItemCard extends StatelessWidget { + final Widget child; + final Info info; + + const ItemCard({ + super.key, + required this.info, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + top: 16, + ), + child: Wrap( + runSpacing: 16, + children: [ + InfoHeader( + info: info, + ), + child, + ], + ), + ); + } +} + +class ThemeColorsBox extends StatefulWidget { + const ThemeColorsBox({super.key}); + + @override + State createState() => _ThemeColorsBoxState(); +} + +class _ThemeColorsBoxState extends State { + + Widget _themeModeCheckBox({ bool? isSelected, required ThemeModeItem themeModeItem, }) { @@ -32,7 +121,7 @@ class ThemeFragment extends StatelessWidget { globalState.appController.config.themeMode = themeModeItem.themeMode; }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal:16), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, @@ -55,7 +144,6 @@ class ThemeFragment extends StatelessWidget { } Widget _primaryColorCheckBox({ - required BuildContext context, bool? isSelected, Color? color, }) { @@ -68,28 +156,8 @@ class ThemeFragment extends StatelessWidget { ); } - 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, - ], - ), - ); - } - - Widget _getThemeCard(BuildContext context) { + @override + Widget build(BuildContext context) { List themeModeItems = [ ThemeModeItem( iconData: Icons.auto_mode, @@ -118,8 +186,7 @@ class ThemeFragment extends StatelessWidget { ]; return Column( children: [ - _itemCard( - context: context, + ItemCard( info: Info( label: appLocalizations.themeMode, iconData: Icons.brightness_high, @@ -137,7 +204,6 @@ class ThemeFragment extends StatelessWidget { final themeModeItem = themeModeItems[index]; return _themeModeCheckBox( isSelected: themeMode == themeModeItem.themeMode, - context: context, themeModeItem: themeModeItem, ); }, @@ -151,8 +217,7 @@ class ThemeFragment extends StatelessWidget { }, ), ), - _itemCard( - context: context, + ItemCard( info: Info( label: appLocalizations.themeColor, iconData: Icons.palette, @@ -172,7 +237,6 @@ class ThemeFragment extends StatelessWidget { itemBuilder: (_, index) { final primaryColor = primaryColors[index]; return _primaryColorCheckBox( - context: context, isSelected: currentPrimaryColor == primaryColor?.value, color: primaryColor, ); @@ -191,30 +255,4 @@ class ThemeFragment extends StatelessWidget { ], ); } - - @override - Widget build(BuildContext context) { - final themeCard = _getThemeCard(context); - final previewCard = Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: CommonCard( - info: Info( - label: appLocalizations.preview, - iconData: Icons.looks, - ), - child: Container( - height: 200, - ), - ), - ); - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - previewCard, - themeCard, - ], - ), - ); - } } diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 36c4d71..a6820c9 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -195,5 +195,21 @@ "testUrl": "Test url", "sync": "Sync", "exclude": "Hidden from recent tasks", - "excludeDesc": "When the app is in the background, the app is hidden from the recent task" + "excludeDesc": "When the app is in the background, the app is hidden from the recent task", + "oneColumn": "One column", + "twoColumns": "Two columns", + "threeColumns": "Three columns", + "fourColumns": "Four columns", + "expand": "Standard", + "shrink": "Shrink", + "min": "Min", + "tab": "Tab", + "list": "List", + "delay": "Delay", + "style": "Style", + "size": "Size", + "sort": "Sort", + "columns": "Columns", + "proxiesSetting": "Proxies setting", + "proxyGroup": "Proxy group" } \ 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 22df47f..2f37792 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -195,5 +195,21 @@ "testUrl": "测速链接", "sync": "同步", "exclude": "从最近任务中隐藏", - "excludeDesc": "应用在后台时,从最近任务中隐藏应用" + "excludeDesc": "应用在后台时,从最近任务中隐藏应用", + "oneColumn": "一列", + "twoColumns": "两列", + "threeColumns": "三列", + "fourColumns": "四列", + "expand": "标准", + "shrink": "紧凑", + "min": "最小", + "tab": "标签页", + "list": "列表", + "delay": "延迟", + "style": "风格", + "size": "尺寸", + "sort": "排序", + "columns": "列数", + "proxiesSetting": "代理设置", + "proxyGroup": "代理组" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 3c18f72..ba04783 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -87,6 +87,7 @@ class MessageLookup extends MessageLookupByLibrary { "checkUpdateError": MessageLookupByLibrary.simpleMessage( "The current application is already the latest version"), "checking": MessageLookupByLibrary.simpleMessage("Checking..."), + "columns": MessageLookupByLibrary.simpleMessage("Columns"), "compatible": MessageLookupByLibrary.simpleMessage("Compatibility mode"), "compatibleDesc": MessageLookupByLibrary.simpleMessage( @@ -107,6 +108,7 @@ class MessageLookup extends MessageLookupByLibrary { "days": MessageLookupByLibrary.simpleMessage("Days"), "defaultSort": MessageLookupByLibrary.simpleMessage("Sort by default"), "defaultText": MessageLookupByLibrary.simpleMessage("Default"), + "delay": MessageLookupByLibrary.simpleMessage("Delay"), "delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"), "delete": MessageLookupByLibrary.simpleMessage("Delete"), "desc": MessageLookupByLibrary.simpleMessage( @@ -126,6 +128,7 @@ class MessageLookup extends MessageLookupByLibrary { "excludeDesc": MessageLookupByLibrary.simpleMessage( "When the app is in the background, the app is hidden from the recent task"), "exit": MessageLookupByLibrary.simpleMessage("Exit"), + "expand": MessageLookupByLibrary.simpleMessage("Standard"), "expirationTime": MessageLookupByLibrary.simpleMessage("Expiration time"), "externalController": @@ -142,6 +145,7 @@ class MessageLookup extends MessageLookupByLibrary { "findProcessMode": MessageLookupByLibrary.simpleMessage("Find process"), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage( "There is a risk of flashback after opening"), + "fourColumns": MessageLookupByLibrary.simpleMessage("Four columns"), "general": MessageLookupByLibrary.simpleMessage("General"), "geoData": MessageLookupByLibrary.simpleMessage("GeoData"), "geodataLoader": @@ -162,12 +166,14 @@ class MessageLookup extends MessageLookupByLibrary { "just": MessageLookupByLibrary.simpleMessage("Just"), "language": MessageLookupByLibrary.simpleMessage("Language"), "light": MessageLookupByLibrary.simpleMessage("Light"), + "list": MessageLookupByLibrary.simpleMessage("List"), "logLevel": MessageLookupByLibrary.simpleMessage("LogLevel"), "logcat": MessageLookupByLibrary.simpleMessage("Logcat"), "logcatDesc": MessageLookupByLibrary.simpleMessage( "Disabling will hide the log entry"), "logs": MessageLookupByLibrary.simpleMessage("Logs"), "logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"), + "min": MessageLookupByLibrary.simpleMessage("Min"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("Minimize on exit"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( @@ -195,6 +201,7 @@ class MessageLookup extends MessageLookupByLibrary { "nullProfileDesc": MessageLookupByLibrary.simpleMessage( "No profile, Please add a profile"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"), + "oneColumn": MessageLookupByLibrary.simpleMessage("One column"), "other": MessageLookupByLibrary.simpleMessage("Other"), "outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"), "override": MessageLookupByLibrary.simpleMessage("Override"), @@ -230,6 +237,9 @@ class MessageLookup extends MessageLookupByLibrary { "profiles": MessageLookupByLibrary.simpleMessage("Profiles"), "project": MessageLookupByLibrary.simpleMessage("Project"), "proxies": MessageLookupByLibrary.simpleMessage("Proxies"), + "proxiesSetting": + MessageLookupByLibrary.simpleMessage("Proxies setting"), + "proxyGroup": MessageLookupByLibrary.simpleMessage("Proxy group"), "proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage( "Set the Clash listening port"), @@ -258,16 +268,21 @@ class MessageLookup extends MessageLookupByLibrary { "selected": MessageLookupByLibrary.simpleMessage("Selected"), "settings": MessageLookupByLibrary.simpleMessage("Settings"), "show": MessageLookupByLibrary.simpleMessage("Show"), + "shrink": MessageLookupByLibrary.simpleMessage("Shrink"), "silentLaunch": MessageLookupByLibrary.simpleMessage("SilentLaunch"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("Start in the background"), + "size": MessageLookupByLibrary.simpleMessage("Size"), + "sort": MessageLookupByLibrary.simpleMessage("Sort"), "startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."), + "style": MessageLookupByLibrary.simpleMessage("Style"), "submit": MessageLookupByLibrary.simpleMessage("Submit"), "sync": MessageLookupByLibrary.simpleMessage("Sync"), "systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage( "Attach HTTP proxy to VpnService"), + "tab": MessageLookupByLibrary.simpleMessage("Tab"), "tabAnimation": MessageLookupByLibrary.simpleMessage("Tab animation"), "tabAnimationDesc": MessageLookupByLibrary.simpleMessage( "When enabled, the home tab will add a toggle animation"), @@ -280,12 +295,14 @@ class MessageLookup extends MessageLookupByLibrary { "themeDesc": MessageLookupByLibrary.simpleMessage( "Set dark mode,adjust the color"), "themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"), + "threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"), "tip": MessageLookupByLibrary.simpleMessage("tip"), "tools": MessageLookupByLibrary.simpleMessage("Tools"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"), "tun": MessageLookupByLibrary.simpleMessage("TUN mode"), "tunDesc": MessageLookupByLibrary.simpleMessage( "only effective in administrator mode"), + "twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"), "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage( "unable to update current profile"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 6cc6ed9..b40a44a 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -71,6 +71,7 @@ class MessageLookup extends MessageLookupByLibrary { "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checking": MessageLookupByLibrary.simpleMessage("检测中..."), + "columns": MessageLookupByLibrary.simpleMessage("列数"), "compatible": MessageLookupByLibrary.simpleMessage("兼容模式"), "compatibleDesc": MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力,获得全量的Clash的支持"), @@ -89,6 +90,7 @@ class MessageLookup extends MessageLookupByLibrary { "days": MessageLookupByLibrary.simpleMessage("天"), "defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"), "defaultText": MessageLookupByLibrary.simpleMessage("默认"), + "delay": MessageLookupByLibrary.simpleMessage("延迟"), "delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"), "delete": MessageLookupByLibrary.simpleMessage("删除"), "desc": MessageLookupByLibrary.simpleMessage( @@ -104,6 +106,7 @@ class MessageLookup extends MessageLookupByLibrary { "excludeDesc": MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"), "exit": MessageLookupByLibrary.simpleMessage("退出"), + "expand": MessageLookupByLibrary.simpleMessage("标准"), "expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"), "externalController": MessageLookupByLibrary.simpleMessage("外部控制器"), "externalControllerDesc": @@ -115,6 +118,7 @@ class MessageLookup extends MessageLookupByLibrary { "findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"), + "fourColumns": MessageLookupByLibrary.simpleMessage("四列"), "general": MessageLookupByLibrary.simpleMessage("基础"), "geoData": MessageLookupByLibrary.simpleMessage("地理数据"), "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"), @@ -131,11 +135,13 @@ class MessageLookup extends MessageLookupByLibrary { "just": MessageLookupByLibrary.simpleMessage("刚刚"), "language": MessageLookupByLibrary.simpleMessage("语言"), "light": MessageLookupByLibrary.simpleMessage("浅色"), + "list": MessageLookupByLibrary.simpleMessage("列表"), "logLevel": MessageLookupByLibrary.simpleMessage("日志等级"), "logcat": MessageLookupByLibrary.simpleMessage("日志捕获"), "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), "logs": MessageLookupByLibrary.simpleMessage("日志"), "logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"), + "min": MessageLookupByLibrary.simpleMessage("最小"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), @@ -158,6 +164,7 @@ class MessageLookup extends MessageLookupByLibrary { "nullProfileDesc": MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"), + "oneColumn": MessageLookupByLibrary.simpleMessage("一列"), "other": MessageLookupByLibrary.simpleMessage("其他"), "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "override": MessageLookupByLibrary.simpleMessage("覆写"), @@ -187,6 +194,8 @@ class MessageLookup extends MessageLookupByLibrary { "profiles": MessageLookupByLibrary.simpleMessage("配置"), "project": MessageLookupByLibrary.simpleMessage("项目"), "proxies": MessageLookupByLibrary.simpleMessage("代理"), + "proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"), + "proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"), @@ -207,15 +216,20 @@ class MessageLookup extends MessageLookupByLibrary { "selected": MessageLookupByLibrary.simpleMessage("已选择"), "settings": MessageLookupByLibrary.simpleMessage("设置"), "show": MessageLookupByLibrary.simpleMessage("显示"), + "shrink": MessageLookupByLibrary.simpleMessage("紧凑"), "silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"), + "size": MessageLookupByLibrary.simpleMessage("尺寸"), + "sort": MessageLookupByLibrary.simpleMessage("排序"), "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), + "style": MessageLookupByLibrary.simpleMessage("风格"), "submit": MessageLookupByLibrary.simpleMessage("提交"), "sync": MessageLookupByLibrary.simpleMessage("同步"), "systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"), + "tab": MessageLookupByLibrary.simpleMessage("标签页"), "tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"), "tabAnimationDesc": MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"), @@ -226,11 +240,13 @@ class MessageLookup extends MessageLookupByLibrary { "themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), + "threeColumns": MessageLookupByLibrary.simpleMessage("三列"), "tip": MessageLookupByLibrary.simpleMessage("提示"), "tools": MessageLookupByLibrary.simpleMessage("工具"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), "tun": MessageLookupByLibrary.simpleMessage("TUN模式"), "tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"), + "twoColumns": MessageLookupByLibrary.simpleMessage("两列"), "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 9427514..04e315d 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -2019,6 +2019,166 @@ class AppLocalizations { args: [], ); } + + /// `One column` + String get oneColumn { + return Intl.message( + 'One column', + name: 'oneColumn', + desc: '', + args: [], + ); + } + + /// `Two columns` + String get twoColumns { + return Intl.message( + 'Two columns', + name: 'twoColumns', + desc: '', + args: [], + ); + } + + /// `Three columns` + String get threeColumns { + return Intl.message( + 'Three columns', + name: 'threeColumns', + desc: '', + args: [], + ); + } + + /// `Four columns` + String get fourColumns { + return Intl.message( + 'Four columns', + name: 'fourColumns', + desc: '', + args: [], + ); + } + + /// `Standard` + String get expand { + return Intl.message( + 'Standard', + name: 'expand', + desc: '', + args: [], + ); + } + + /// `Shrink` + String get shrink { + return Intl.message( + 'Shrink', + name: 'shrink', + desc: '', + args: [], + ); + } + + /// `Min` + String get min { + return Intl.message( + 'Min', + name: 'min', + desc: '', + args: [], + ); + } + + /// `Tab` + String get tab { + return Intl.message( + 'Tab', + name: 'tab', + desc: '', + args: [], + ); + } + + /// `List` + String get list { + return Intl.message( + 'List', + name: 'list', + desc: '', + args: [], + ); + } + + /// `Delay` + String get delay { + return Intl.message( + 'Delay', + name: 'delay', + desc: '', + args: [], + ); + } + + /// `Style` + String get style { + return Intl.message( + 'Style', + name: 'style', + desc: '', + args: [], + ); + } + + /// `Size` + String get size { + return Intl.message( + 'Size', + name: 'size', + desc: '', + args: [], + ); + } + + /// `Sort` + String get sort { + return Intl.message( + 'Sort', + name: 'sort', + desc: '', + args: [], + ); + } + + /// `Columns` + String get columns { + return Intl.message( + 'Columns', + name: 'columns', + desc: '', + args: [], + ); + } + + /// `Proxies setting` + String get proxiesSetting { + return Intl.message( + 'Proxies setting', + name: 'proxiesSetting', + desc: '', + args: [], + ); + } + + /// `Proxy group` + String get proxyGroup { + return Intl.message( + 'Proxy group', + name: 'proxyGroup', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/main.dart b/lib/main.dart index 86ebc3f..346b85e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/plugins/app.dart'; @@ -14,12 +15,12 @@ import 'common/common.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await android?.init(); - await window?.init(); clashCore.initMessage(); globalState.packageInfo = await PackageInfo.fromPlatform(); final config = await preferences.getConfig() ?? Config(); final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); + await android?.init(); + await window?.init(config.windowProps); final appState = AppState( mode: clashConfig.mode, isCompatible: config.isCompatible, @@ -110,6 +111,8 @@ Future vpnService() async { onStop: () async { await app?.tip(appLocalizations.stopVpn); await globalState.stopSystemProxy(); + clashCore.shutdown(); + exit(0); }, ), ); diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index f21e3e1..4ad29a7 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -311,6 +311,13 @@ class ClashConfig extends ChangeNotifier { _mode = clashConfig._mode; _logLevel = clashConfig._logLevel; _tun = clashConfig._tun; + _findProcessMode = clashConfig._findProcessMode; + _geoXUrl = clashConfig._geoXUrl; + _unifiedDelay = clashConfig._unifiedDelay; + _globalRealUa = clashConfig._globalRealUa; + _tcpConcurrent = clashConfig._tcpConcurrent; + _externalController = clashConfig._externalController; + _geodataLoader = clashConfig._geodataLoader; _dns = clashConfig._dns; _rules = clashConfig._rules; _globalRealUa = clashConfig.globalRealUa; diff --git a/lib/models/config.dart b/lib/models/config.dart index 71ccc4e..ae9cefb 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -36,6 +36,21 @@ class Props with _$Props { factory Props.fromJson(Map json) => _$PropsFromJson(json); } +@freezed +class WindowProps with _$WindowProps { + const factory WindowProps({ + @Default(1000) double width, + @Default(600) double height, + double? top, + double? left, + }) = _WindowProps; + + factory WindowProps.fromJson(Map? json) => + json == null ? defaultWindowProps : _$WindowPropsFromJson(json); +} + +const defaultWindowProps = WindowProps(); + @JsonSerializable() class Config extends ChangeNotifier { List _profiles; @@ -62,6 +77,7 @@ class Config extends ChangeNotifier { ProxyCardType _proxyCardType; int _proxiesColumns; String _testUrl; + WindowProps _windowProps; Config() : _profiles = [], @@ -83,6 +99,7 @@ class Config extends ChangeNotifier { _allowBypass = true, _isExclude = false, _proxyCardType = ProxyCardType.expand, + _windowProps = defaultWindowProps, _proxiesType = ProxiesType.tab, _proxiesColumns = 2; @@ -388,7 +405,10 @@ class Config extends ChangeNotifier { } } - @JsonKey(defaultValue: ProxiesType.tab) + @JsonKey( + defaultValue: ProxiesType.tab, + unknownEnumValue: ProxiesType.tab, + ) ProxiesType get proxiesType => _proxiesType; set proxiesType(ProxiesType value) { @@ -438,6 +458,15 @@ class Config extends ChangeNotifier { } } + WindowProps get windowProps => _windowProps; + + set windowProps(WindowProps value) { + if (_windowProps != value) { + _windowProps = value; + notifyListeners(); + } + } + update([ Config? config, RecoveryOption recoveryOptions = RecoveryOption.all, @@ -470,7 +499,9 @@ class Config extends ChangeNotifier { _isAnimateToPage = config._isAnimateToPage; _autoCheckUpdate = config._autoCheckUpdate; _dav = config._dav; - _testUrl = config.testUrl; + _testUrl = config._testUrl; + _isExclude = config._isExclude; + _windowProps = config._windowProps; } notifyListeners(); } @@ -482,9 +513,4 @@ class Config extends ChangeNotifier { factory Config.fromJson(Map json) { return _$ConfigFromJson(json); } - - @override - String toString() { - return 'Config{_profiles: $_profiles, _isCompatible: $_isCompatible, _currentProfileId: $_currentProfileId, _autoLaunch: $_autoLaunch, _silentLaunch: $_silentLaunch, _autoRun: $_autoRun, _openLog: $_openLog, _themeMode: $_themeMode, _locale: $_locale, _primaryColor: $_primaryColor, _proxiesSortType: $_proxiesSortType, _isMinimizeOnExit: $_isMinimizeOnExit, _isAccessControl: $_isAccessControl, _accessControl: $_accessControl, _isAnimateToPage: $_isAnimateToPage, _dav: $_dav}'; - } } diff --git a/lib/models/connection.dart b/lib/models/connection.dart index 49f1f79..1b2bad5 100644 --- a/lib/models/connection.dart +++ b/lib/models/connection.dart @@ -48,7 +48,11 @@ class ConnectionsAndKeywords with _$ConnectionsAndKeywords { _$ConnectionsAndKeywordsFromJson(json); } - -extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords{ - List get filteredConnections => connections.where((connection)=> Set.from(connection.chains).containsAll(keywords)).toList(); -} \ No newline at end of file +extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords { + List get filteredConnections => connections + .where((connection) => { + ...connection.chains, + connection.metadata.process, + }.containsAll(keywords)) + .toList(); +} diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index 3ec63a1..37f8f51 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -429,3 +429,194 @@ abstract class _Props implements Props { _$$PropsImplCopyWith<_$PropsImpl> get copyWith => throw _privateConstructorUsedError; } + +WindowProps _$WindowPropsFromJson(Map json) { + return _WindowProps.fromJson(json); +} + +/// @nodoc +mixin _$WindowProps { + double get width => throw _privateConstructorUsedError; + double get height => throw _privateConstructorUsedError; + double? get top => throw _privateConstructorUsedError; + double? get left => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $WindowPropsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $WindowPropsCopyWith<$Res> { + factory $WindowPropsCopyWith( + WindowProps value, $Res Function(WindowProps) then) = + _$WindowPropsCopyWithImpl<$Res, WindowProps>; + @useResult + $Res call({double width, double height, double? top, double? left}); +} + +/// @nodoc +class _$WindowPropsCopyWithImpl<$Res, $Val extends WindowProps> + implements $WindowPropsCopyWith<$Res> { + _$WindowPropsCopyWithImpl(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? width = null, + Object? height = null, + Object? top = freezed, + Object? left = freezed, + }) { + return _then(_value.copyWith( + width: null == width + ? _value.width + : width // ignore: cast_nullable_to_non_nullable + as double, + height: null == height + ? _value.height + : height // ignore: cast_nullable_to_non_nullable + as double, + top: freezed == top + ? _value.top + : top // ignore: cast_nullable_to_non_nullable + as double?, + left: freezed == left + ? _value.left + : left // ignore: cast_nullable_to_non_nullable + as double?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$WindowPropsImplCopyWith<$Res> + implements $WindowPropsCopyWith<$Res> { + factory _$$WindowPropsImplCopyWith( + _$WindowPropsImpl value, $Res Function(_$WindowPropsImpl) then) = + __$$WindowPropsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({double width, double height, double? top, double? left}); +} + +/// @nodoc +class __$$WindowPropsImplCopyWithImpl<$Res> + extends _$WindowPropsCopyWithImpl<$Res, _$WindowPropsImpl> + implements _$$WindowPropsImplCopyWith<$Res> { + __$$WindowPropsImplCopyWithImpl( + _$WindowPropsImpl _value, $Res Function(_$WindowPropsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? width = null, + Object? height = null, + Object? top = freezed, + Object? left = freezed, + }) { + return _then(_$WindowPropsImpl( + width: null == width + ? _value.width + : width // ignore: cast_nullable_to_non_nullable + as double, + height: null == height + ? _value.height + : height // ignore: cast_nullable_to_non_nullable + as double, + top: freezed == top + ? _value.top + : top // ignore: cast_nullable_to_non_nullable + as double?, + left: freezed == left + ? _value.left + : left // ignore: cast_nullable_to_non_nullable + as double?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$WindowPropsImpl implements _WindowProps { + const _$WindowPropsImpl( + {this.width = 1000, this.height = 600, this.top, this.left}); + + factory _$WindowPropsImpl.fromJson(Map json) => + _$$WindowPropsImplFromJson(json); + + @override + @JsonKey() + final double width; + @override + @JsonKey() + final double height; + @override + final double? top; + @override + final double? left; + + @override + String toString() { + return 'WindowProps(width: $width, height: $height, top: $top, left: $left)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$WindowPropsImpl && + (identical(other.width, width) || other.width == width) && + (identical(other.height, height) || other.height == height) && + (identical(other.top, top) || other.top == top) && + (identical(other.left, left) || other.left == left)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, width, height, top, left); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$WindowPropsImplCopyWith<_$WindowPropsImpl> get copyWith => + __$$WindowPropsImplCopyWithImpl<_$WindowPropsImpl>(this, _$identity); + + @override + Map toJson() { + return _$$WindowPropsImplToJson( + this, + ); + } +} + +abstract class _WindowProps implements WindowProps { + const factory _WindowProps( + {final double width, + final double height, + final double? top, + final double? left}) = _$WindowPropsImpl; + + factory _WindowProps.fromJson(Map json) = + _$WindowPropsImpl.fromJson; + + @override + double get width; + @override + double get height; + @override + double? get top; + @override + double? get left; + @override + @JsonKey(ignore: true) + _$$WindowPropsImplCopyWith<_$WindowPropsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 1926f60..e1dbd4c 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -35,16 +35,18 @@ Config _$ConfigFromJson(Map json) => Config() ..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true ..allowBypass = json['allowBypass'] as bool? ?? true ..systemProxy = json['systemProxy'] as bool? ?? true - ..proxiesType = - $enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType']) ?? - ProxiesType.tab + ..proxiesType = $enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType'], + unknownValue: ProxiesType.tab) ?? + ProxiesType.tab ..proxyCardType = $enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ?? ProxyCardType.expand ..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2 ..testUrl = json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204' - ..isExclude = json['isExclude'] as bool? ?? false; + ..isExclude = json['isExclude'] as bool? ?? false + ..windowProps = + WindowProps.fromJson(json['windowProps'] as Map?); Map _$ConfigToJson(Config instance) => { 'profiles': instance.profiles, @@ -71,6 +73,7 @@ Map _$ConfigToJson(Config instance) => { 'proxiesColumns': instance.proxiesColumns, 'test-url': instance.testUrl, 'isExclude': instance.isExclude, + 'windowProps': instance.windowProps, }; const _$ThemeModeEnumMap = { @@ -87,12 +90,13 @@ const _$ProxiesSortTypeEnumMap = { const _$ProxiesTypeEnumMap = { ProxiesType.tab: 'tab', - ProxiesType.expansion: 'expansion', + ProxiesType.list: 'list', }; const _$ProxyCardTypeEnumMap = { ProxyCardType.expand: 'expand', ProxyCardType.shrink: 'shrink', + ProxyCardType.min: 'min', }; _$AccessControlImpl _$$AccessControlImplFromJson(Map json) => @@ -138,3 +142,19 @@ Map _$$PropsImplToJson(_$PropsImpl instance) => 'allowBypass': instance.allowBypass, 'systemProxy': instance.systemProxy, }; + +_$WindowPropsImpl _$$WindowPropsImplFromJson(Map json) => + _$WindowPropsImpl( + width: (json['width'] as num?)?.toDouble() ?? 1000, + height: (json['height'] as num?)?.toDouble() ?? 600, + top: (json['top'] as num?)?.toDouble(), + left: (json['left'] as num?)?.toDouble(), + ); + +Map _$$WindowPropsImplToJson(_$WindowPropsImpl instance) => + { + 'width': instance.width, + 'height': instance.height, + 'top': instance.top, + 'left': instance.left, + }; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index ae9e215..bb9133b 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -2261,3 +2261,143 @@ abstract class _PackageListSelectorState implements PackageListSelectorState { _$$PackageListSelectorStateImplCopyWith<_$PackageListSelectorStateImpl> get copyWith => throw _privateConstructorUsedError; } + +/// @nodoc +mixin _$ColumnsSelectorState { + int get columns => throw _privateConstructorUsedError; + ViewMode get viewMode => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ColumnsSelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ColumnsSelectorStateCopyWith<$Res> { + factory $ColumnsSelectorStateCopyWith(ColumnsSelectorState value, + $Res Function(ColumnsSelectorState) then) = + _$ColumnsSelectorStateCopyWithImpl<$Res, ColumnsSelectorState>; + @useResult + $Res call({int columns, ViewMode viewMode}); +} + +/// @nodoc +class _$ColumnsSelectorStateCopyWithImpl<$Res, + $Val extends ColumnsSelectorState> + implements $ColumnsSelectorStateCopyWith<$Res> { + _$ColumnsSelectorStateCopyWithImpl(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? columns = null, + Object? viewMode = null, + }) { + return _then(_value.copyWith( + columns: null == columns + ? _value.columns + : columns // ignore: cast_nullable_to_non_nullable + as int, + viewMode: null == viewMode + ? _value.viewMode + : viewMode // ignore: cast_nullable_to_non_nullable + as ViewMode, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ColumnsSelectorStateImplCopyWith<$Res> + implements $ColumnsSelectorStateCopyWith<$Res> { + factory _$$ColumnsSelectorStateImplCopyWith(_$ColumnsSelectorStateImpl value, + $Res Function(_$ColumnsSelectorStateImpl) then) = + __$$ColumnsSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int columns, ViewMode viewMode}); +} + +/// @nodoc +class __$$ColumnsSelectorStateImplCopyWithImpl<$Res> + extends _$ColumnsSelectorStateCopyWithImpl<$Res, _$ColumnsSelectorStateImpl> + implements _$$ColumnsSelectorStateImplCopyWith<$Res> { + __$$ColumnsSelectorStateImplCopyWithImpl(_$ColumnsSelectorStateImpl _value, + $Res Function(_$ColumnsSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? columns = null, + Object? viewMode = null, + }) { + return _then(_$ColumnsSelectorStateImpl( + columns: null == columns + ? _value.columns + : columns // ignore: cast_nullable_to_non_nullable + as int, + viewMode: null == viewMode + ? _value.viewMode + : viewMode // ignore: cast_nullable_to_non_nullable + as ViewMode, + )); + } +} + +/// @nodoc + +class _$ColumnsSelectorStateImpl implements _ColumnsSelectorState { + const _$ColumnsSelectorStateImpl( + {required this.columns, required this.viewMode}); + + @override + final int columns; + @override + final ViewMode viewMode; + + @override + String toString() { + return 'ColumnsSelectorState(columns: $columns, viewMode: $viewMode)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ColumnsSelectorStateImpl && + (identical(other.columns, columns) || other.columns == columns) && + (identical(other.viewMode, viewMode) || + other.viewMode == viewMode)); + } + + @override + int get hashCode => Object.hash(runtimeType, columns, viewMode); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ColumnsSelectorStateImplCopyWith<_$ColumnsSelectorStateImpl> + get copyWith => + __$$ColumnsSelectorStateImplCopyWithImpl<_$ColumnsSelectorStateImpl>( + this, _$identity); +} + +abstract class _ColumnsSelectorState implements ColumnsSelectorState { + const factory _ColumnsSelectorState( + {required final int columns, + required final ViewMode viewMode}) = _$ColumnsSelectorStateImpl; + + @override + int get columns; + @override + ViewMode get viewMode; + @override + @JsonKey(ignore: true) + _$$ColumnsSelectorStateImplCopyWith<_$ColumnsSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/models/selector.dart b/lib/models/selector.dart index 622503b..63f94a2 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -124,3 +124,12 @@ class PackageListSelectorState with _$PackageListSelectorState { required bool isAccessControl, }) = _PackageListSelectorState; } + + +@freezed +class ColumnsSelectorState with _$ColumnsSelectorState { + const factory ColumnsSelectorState({ + required int columns, + required ViewMode viewMode, + }) = _ColumnsSelectorState; +} diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 5060cde..ef656e6 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -14,6 +14,7 @@ class HomePage extends StatelessWidget { const HomePage({super.key}); _getNavigationBar({ + required BuildContext context, required ViewMode viewMode, required List navigationItems, required int currentIndex, @@ -34,6 +35,8 @@ class HomePage extends StatelessWidget { } final extended = viewMode == ViewMode.desktop; return NavigationRail( + backgroundColor: context.colorScheme.surfaceContainer, + groupAlignment: -0.8, destinations: navigationItems .map( (e) => NavigationRailDestination( @@ -82,32 +85,37 @@ class HomePage extends StatelessWidget { ); final currentIndex = index == -1 ? 0 : index; final navigationBar = _getNavigationBar( + context: context, viewMode: viewMode, navigationItems: navigationItems, currentIndex: currentIndex, ); final bottomNavigationBar = viewMode == ViewMode.mobile ? navigationBar : null; - Widget body; if (viewMode != ViewMode.mobile) { - body = Row( + return Row( children: [ navigationBar, Expanded( flex: 1, - child: child!, + child: CommonScaffold( + key: globalState.homeScaffoldKey, + title: Intl.message( + currentLabel, + ), + body: child!, + bottomNavigationBar: bottomNavigationBar, + ), ) ], ); - } else { - body = child!; } return CommonScaffold( key: globalState.homeScaffoldKey, title: Intl.message( currentLabel, ), - body: body, + body: child!, bottomNavigationBar: bottomNavigationBar, ); }, diff --git a/lib/state.dart b/lib/state.dart index d9c044e..1e17ed9 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -92,7 +92,7 @@ class GlobalState { appState: appState, config: config, clashConfig: clashConfig, - ).then((_){ + ).then((_) { globalState.appController.addCheckIpNumDebounce(); }); } @@ -252,18 +252,6 @@ class GlobalState { return null; } } - - int getColumns(ViewMode viewMode, int currentColumns) { - final targetColumnsArray = switch (viewMode) { - ViewMode.mobile => [2, 1], - ViewMode.laptop => [3, 2], - ViewMode.desktop => [4, 3], - }; - if (targetColumnsArray.contains(currentColumns)) { - return currentColumns; - } - return targetColumnsArray.first; - } } final globalState = GlobalState(); diff --git a/lib/widgets/connection_item.dart b/lib/widgets/connection_item.dart new file mode 100644 index 0000000..c9bf8ef --- /dev/null +++ b/lib/widgets/connection_item.dart @@ -0,0 +1,167 @@ +import 'dart:io'; + +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/plugins/app.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'chip.dart'; +import 'list.dart'; + +class ConnectionItem extends StatelessWidget { + final Connection connection; + final Function(String)? onClick; + final Widget? trailing; + + const ConnectionItem({ + super.key, + required this.connection, + this.onClick, + this.trailing, + }); + + Future _getPackageIcon(Connection connection) async { + return await app?.getPackageIcon(connection.metadata.process); + } + + String _getRequestText(Metadata metadata) { + var text = "${metadata.network}://"; + final ips = [ + metadata.host, + metadata.destinationIP, + ].where((ip) => ip.isNotEmpty); + text += ips.join("/"); + text += ":${metadata.destinationPort}"; + return text; + } + + String _getSourceText(Connection connection) { + final metadata = connection.metadata; + if (metadata.process.isEmpty) { + return connection.start.lastUpdateTimeDesc; + } + return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}"; + } + + @override + Widget build(BuildContext context) { + if (!Platform.isAndroid) { + return ListItem( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + tileTitleAlignment: ListTileTitleAlignment.titleHeight, + title: Text( + _getRequestText(connection.metadata), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8, + ), + Text( + _getSourceText(connection), + ), + const SizedBox( + height: 8, + ), + Wrap( + runSpacing: 6, + spacing: 6, + children: [ + for (final chain in connection.chains) + CommonChip( + label: chain, + onPressed: () { + if (onClick == null) return; + onClick!(chain); + }, + ), + ], + ), + ], + ), + trailing: trailing, + ); + } + return Selector( + selector: (_, clashConfig) => + clashConfig.findProcessMode == FindProcessMode.always, + builder: (_, value, child) { + return ListItem( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + tileTitleAlignment: ListTileTitleAlignment.titleHeight, + leading: value + ? GestureDetector( + onTap: () { + if (onClick == null) return; + final process = connection.metadata.process; + if(process.isEmpty) return; + onClick!(process); + }, + child: Container( + margin: const EdgeInsets.only(top: 4), + width: 48, + height: 48, + child: FutureBuilder( + future: _getPackageIcon(connection), + builder: (_, snapshot) { + if (!snapshot.hasData && snapshot.data == null) { + return Container(); + } else { + return Image( + image: snapshot.data!, + gaplessPlayback: true, + width: 48, + height: 48, + ); + } + }, + ), + ), + ) + : null, + title: Text( + _getRequestText(connection.metadata), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8, + ), + Text( + _getSourceText(connection), + ), + const SizedBox( + height: 8, + ), + Wrap( + runSpacing: 6, + spacing: 6, + children: [ + for (final chain in connection.chains) + CommonChip( + label: chain, + onPressed: () { + if (onClick == null) return; + onClick!(chain); + }, + ), + ], + ), + ], + ), + trailing: trailing, + ); + }, + ); + } +} diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index 06787bf..be9eb43 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -5,7 +5,7 @@ import 'package:fl_clash/widgets/open_container.dart'; import 'package:flutter/material.dart'; import 'card.dart'; -import 'extend_page.dart'; +import 'sheet.dart'; import 'scaffold.dart'; class Delegate { diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index ae6a0b9..50e6971 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -119,14 +119,21 @@ class CommonScaffoldState extends State { child: Stack( alignment: Alignment.bottomCenter, children: [ - ValueListenableBuilder( + ValueListenableBuilder>( valueListenable: _actions, builder: (_, actions, __) { + final realActions = + actions.isNotEmpty ? actions : widget.actions; return AppBar( automaticallyImplyLeading: widget.automaticallyImplyLeading, leading: widget.leading, title: Text(widget.title), - actions: actions.isNotEmpty ? actions : widget.actions, + actions: [ + ...?realActions, + const SizedBox( + width: 8, + ) + ], ); }, ), diff --git a/lib/widgets/extend_page.dart b/lib/widgets/sheet.dart similarity index 70% rename from lib/widgets/extend_page.dart rename to lib/widgets/sheet.dart index ec6d09f..dee4e52 100644 --- a/lib/widgets/extend_page.dart +++ b/lib/widgets/sheet.dart @@ -54,3 +54,34 @@ showExtendPage( ), ); } + +showSheet({ + required BuildContext context, + required WidgetBuilder builder, + required String title, + bool isScrollControlled = true, + double width = 320, +}) { + final viewMode = globalState.appController.appState.viewMode; + final isMobile = viewMode == ViewMode.mobile; + if (isMobile) { + showModalBottomSheet( + context: context, + isScrollControlled: isScrollControlled, + builder: builder, + showDragHandle: true, + useSafeArea: true, + ); + } else { + showModalSideSheet( + useSafeArea: true, + isScrollControlled: isScrollControlled, + context: context, + constraints: BoxConstraints( + maxWidth: width, + ), + body: builder(context), + title: title, + ); + } +} diff --git a/lib/widgets/side_sheet.dart b/lib/widgets/side_sheet.dart index cc6dbd0..e243280 100644 --- a/lib/widgets/side_sheet.dart +++ b/lib/widgets/side_sheet.dart @@ -84,8 +84,11 @@ class _SideSheetState extends State { borderRadius: BorderRadius.circular(0), ); - final BoxConstraints constraints = - widget.constraints ?? const BoxConstraints(maxWidth: 320); + final BoxConstraints constraints = widget.constraints ?? + const BoxConstraints( + maxWidth: 320, + minWidth: 320 + ); final Clip clipBehavior = widget.clipBehavior ?? Clip.none; @@ -403,27 +406,26 @@ class _ModalSideSheetState extends State<_ModalSideSheet> { } class ModalSideSheetRoute extends PopupRoute { - ModalSideSheetRoute({ - required this.builder, - this.capturedThemes, - this.barrierLabel, - this.barrierOnTapHint, - this.backgroundColor, - this.elevation, - this.shape, - this.clipBehavior, - this.constraints, - this.modalBarrierColor, - this.isDismissible = true, - this.isScrollControlled = false, - this.scrollControlDisabledMaxHeightRatio = - _defaultScrollControlDisabledMaxHeightRatio, - super.settings, - this.transitionAnimationController, - this.anchorPoint, - this.useSafeArea = false, - super.filter - }); + ModalSideSheetRoute( + {required this.builder, + this.capturedThemes, + this.barrierLabel, + this.barrierOnTapHint, + this.backgroundColor, + this.elevation, + this.shape, + this.clipBehavior, + this.constraints, + this.modalBarrierColor, + this.isDismissible = true, + this.isScrollControlled = false, + this.scrollControlDisabledMaxHeightRatio = + _defaultScrollControlDisabledMaxHeightRatio, + super.settings, + this.transitionAnimationController, + this.anchorPoint, + this.useSafeArea = false, + super.filter}); final WidgetBuilder builder; @@ -601,7 +603,9 @@ Future showModalSideSheet({ width: kToolbarHeight, child: BackButton(), ), - const SizedBox(width: 8,), + const SizedBox( + width: 8, + ), Expanded( child: Text( title, @@ -617,6 +621,7 @@ Future showModalSideSheet({ ), ), Expanded( + flex: 1, child: body, ), ], diff --git a/lib/widgets/tray_container.dart b/lib/widgets/tray_container.dart index 53e05fd..9d5a148 100644 --- a/lib/widgets/tray_container.dart +++ b/lib/widgets/tray_container.dart @@ -35,6 +35,9 @@ class _TrayContainerState extends State with TrayListener { await trayManager.setIcon( other.getTrayIconPath(), ); + await trayManager.setToolTip( + appName, + ); isTrayInit = true; } } @@ -44,6 +47,9 @@ class _TrayContainerState extends State with TrayListener { await trayManager.setIcon( other.getTrayIconPath(), ); + await trayManager.setToolTip( + appName, + ); } updateMenu(TrayContainerSelectorState state) async { diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 0e6844f..0b22e1c 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -11,7 +11,7 @@ export 'null_status.dart'; export 'pop_container.dart'; export 'disabled_mask.dart'; export 'side_sheet.dart'; -export 'extend_page.dart'; +export 'sheet.dart'; export 'keep_container.dart'; export 'animate_grid.dart'; export 'tray_container.dart'; @@ -22,4 +22,5 @@ export 'tile_container.dart'; export 'chip.dart'; export 'fade_box.dart'; export 'app_state_container.dart'; -export 'text.dart'; \ No newline at end of file +export 'text.dart'; +export 'connection_item.dart'; \ No newline at end of file diff --git a/lib/widgets/window_container.dart b/lib/widgets/window_container.dart index 28a37a0..9ea82f6 100644 --- a/lib/widgets/window_container.dart +++ b/lib/widgets/window_container.dart @@ -18,7 +18,6 @@ class WindowContainer extends StatefulWidget { } class _WindowContainerState extends State with WindowListener { - _autoLaunchContainer(Widget child) { return Selector( selector: (_, config) => config.autoLaunch, @@ -47,6 +46,28 @@ class _WindowContainerState extends State with WindowListener { super.onWindowClose(); } + @override + Future onWindowMoved() async { + super.onWindowMoved(); + final offset = await windowManager.getPosition(); + final config = globalState.appController.config; + config.windowProps = config.windowProps.copyWith( + top: offset.dy, + left: offset.dx, + ); + } + + @override + Future onWindowResized() async { + super.onWindowResized(); + final size = await windowManager.getSize(); + final config = globalState.appController.config; + config.windowProps = config.windowProps.copyWith( + width: size.width, + height: size.height, + ); + } + @override void onWindowMinimize() async { await globalState.appController.savePreferences(); diff --git a/pubspec.yaml b/pubspec.yaml index 7fd4e31..a6f83df 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.40+202407152 +version: 0.8.41+202407171 environment: sdk: '>=3.1.0 <4.0.0'