Compare commits

...

5 Commits

Author SHA1 Message Date
chen08209
82be4cc45f Optimize proxies page
Support mouse drag scroll

Adjust desktop ui
2024-07-17 14:52:15 +08:00
chen08209
2c3f4ae8a8 Revert "Fix android vpn issues"
This reverts commit 891977408e.
2024-07-17 14:51:52 +08:00
chen08209
891977408e Fix android vpn issues 2024-07-15 16:19:58 +08:00
chen08209
5292f34e8d Fix android vpn issues 2024-07-15 16:18:51 +08:00
chen08209
1c54db6bf3 Rollback partial modification 2024-07-15 16:14:19 +08:00
48 changed files with 2325 additions and 1222 deletions

View File

@@ -44,7 +44,7 @@ object GlobalState {
fun initServiceEngine(context: Context) { fun initServiceEngine(context: Context) {
if (serviceEngine != null) return if (serviceEngine != null) return
lock.withLock { lock.withLock {
if (serviceEngine != null) return destroyServiceEngine()
serviceEngine = FlutterEngine(context) serviceEngine = FlutterEngine(context)
serviceEngine?.plugins?.add(ProxyPlugin()) serviceEngine?.plugins?.add(ProxyPlugin())
serviceEngine?.plugins?.add(AppPlugin()) serviceEngine?.plugins?.add(AppPlugin())

View File

@@ -175,11 +175,14 @@ class FlClashVpnService : VpnService() {
fun getService(): FlClashVpnService = this@FlClashVpnService fun getService(): FlClashVpnService = this@FlClashVpnService
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
CoroutineScope(Dispatchers.Main).launch {
GlobalState.getCurrentTitlePlugin()?.handleStop()
}
try { try {
return super.onTransact(code, data, reply, flags) val isSuccess = super.onTransact(code, data, reply, flags)
if (!isSuccess) {
CoroutineScope(Dispatchers.Main).launch {
GlobalState.getCurrentTitlePlugin()?.handleStop()
}
}
return isSuccess
} catch (e: RemoteException) { } catch (e: RemoteException) {
throw e throw e
} }
@@ -192,7 +195,6 @@ class FlClashVpnService : VpnService() {
} }
override fun onUnbind(intent: Intent?): Boolean { override fun onUnbind(intent: Intent?): Boolean {
GlobalState.getCurrentTitlePlugin()?.handleStop()
return super.onUnbind(intent) return super.onUnbind(intent)
} }

View File

@@ -149,6 +149,7 @@ class ApplicationState extends State<Application> {
builder: (lightDynamic, darkDynamic) { builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic); _updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey, navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [ localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,
@@ -156,6 +157,7 @@ class ApplicationState extends State<Application> {
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate GlobalWidgetsLocalizations.delegate
], ],
scrollBehavior: BaseScrollBehavior(),
title: appName, title: appName,
locale: other.getLocaleForString(state.locale), locale: other.getLocaleForString(state.locale),
supportedLocales: supportedLocales:

View File

@@ -24,4 +24,5 @@ export 'function.dart';
export 'package.dart'; export 'package.dart';
export 'measure.dart'; export 'measure.dart';
export 'service.dart'; export 'service.dart';
export 'iterable.dart'; export 'iterable.dart';
export 'scroll.dart';

View File

@@ -1,5 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/clash_config.dart'; import 'package:fl_clash/models/clash_config.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -15,10 +16,14 @@ const asnFileName = "ASN.mmdb";
const geoIpFileName = "GeoIP.dat"; const geoIpFileName = "GeoIP.dat";
const geoSiteFileName = "GeoSite.dat"; const geoSiteFileName = "GeoSite.dat";
const GeoXMap defaultGeoXMap = { const GeoXMap defaultGeoXMap = {
"mmdb":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", "mmdb":
"asn":"https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb", "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"geoip":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat", "asn":
"geosite":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat" "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 profilesDirectoryName = "profiles";
const localhost = "127.0.0.1"; const localhost = "127.0.0.1";
@@ -39,4 +44,10 @@ final filter = ImageFilter.blur(
tileMode: TileMode.mirror, tileMode: TileMode.mirror,
); );
const viewModeColumnsMap = {
ViewMode.mobile: [2, 1],
ViewMode.laptop: [3, 2],
ViewMode.desktop: [4, 3],
};
const defaultPrimaryColor = Colors.brown; const defaultPrimaryColor = Colors.brown;

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -189,6 +190,24 @@ class Other {
if (viewWidth <= maxLaptopWidth) return ViewMode.laptop; if (viewWidth <= maxLaptopWidth) return ViewMode.laptop;
return ViewMode.desktop; 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(); final other = Other();

16
lib/common/scroll.dart Normal file
View File

@@ -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<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.invertedStylus,
PointerDeviceKind.trackpad,
if (system.isDesktop) PointerDeviceKind.mouse,
PointerDeviceKind.unknown,
};
}

View File

@@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/models/config.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:windows_single_instance/windows_single_instance.dart'; import 'package:windows_single_instance/windows_single_instance.dart';
@@ -8,7 +9,7 @@ import 'protocol.dart';
import 'system.dart'; import 'system.dart';
class Window { class Window {
init() async { init(WindowProps props) async {
if (Platform.isWindows) { if (Platform.isWindows) {
await WindowsSingleInstance.ensureSingleInstance([], "FlClash"); await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
protocol.register("clash"); protocol.register("clash");
@@ -16,11 +17,17 @@ class Window {
protocol.register("flclash"); protocol.register("flclash");
} }
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions( WindowOptions windowOptions = WindowOptions(
size: Size(1000, 600), size: Size(props.width, props.height),
minimumSize: Size(400, 600), minimumSize: const Size(380, 600),
center: true,
); );
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.waitUntilReadyToShow(windowOptions, () async {
await windowManager.setPreventClose(true); await windowManager.setPreventClose(true);
}); });

View File

@@ -411,14 +411,7 @@ class AppController {
} }
int get columns => int get columns =>
globalState.getColumns(appState.viewMode, config.proxiesColumns); other.getColumns(appState.viewMode, config.proxiesColumns);
changeColumns() {
config.proxiesColumns = globalState.getColumns(
appState.viewMode,
columns - 1,
);
}
updateViewWidth(double width) { updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {

View File

@@ -82,6 +82,6 @@ enum ChipType { action, delete }
enum CommonCardType { plain, filled } enum CommonCardType { plain, filled }
enum ProxiesType { tab, expansion } enum ProxiesType { tab, list }
enum ProxyCardType { expand, shrink } enum ProxyCardType { expand, shrink, min }

View File

@@ -1,11 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -63,9 +61,6 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
), ),
const SizedBox(
width: 8,
)
]; ];
}, },
); );
@@ -162,7 +157,12 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
key: Key(connection.id), key: Key(connection.id),
connection: connection, connection: connection,
onClick: _addKeyword, onClick: _addKeyword,
onBlock: _handleBlockConnection, trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
); );
}, },
separatorBuilder: (BuildContext context, int index) { separatorBuilder: (BuildContext context, int index) {
@@ -181,113 +181,6 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
} }
} }
class ConnectionItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClick;
final Function(String)? onBlock;
const ConnectionItem({
super.key,
required this.connection,
this.onClick,
this.onBlock,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
return await app?.getPackageIcon(connection.metadata.process);
}
String _getRequestText(Metadata metadata) {
var text = "${metadata.network}://";
final ips = [
metadata.host,
metadata.destinationIP,
].where((ip) => ip.isNotEmpty);
text += ips.join("/");
text += ":${metadata.destinationPort}";
return text;
}
String _getSourceText(Connection connection) {
final metadata = connection.metadata;
if (metadata.process.isEmpty) {
return connection.start.lastUpdateTimeDesc;
}
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
}
@override
Widget build(BuildContext context) {
return ListItem(
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<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
)
: null,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
),
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
if (onBlock == null) return;
onBlock!(connection.id);
},
),
);
}
}
class ConnectionsSearchDelegate extends SearchDelegate { class ConnectionsSearchDelegate extends SearchDelegate {
ValueNotifier<ConnectionsAndKeywords> connectionsNotifier; ValueNotifier<ConnectionsAndKeywords> connectionsNotifier;
@@ -333,11 +226,11 @@ class ConnectionsSearchDelegate extends SearchDelegate {
); );
} }
_handleBlockConnection(String id) { _handleBlockConnection(String id) {
clashCore.closeConnections(id); clashCore.closeConnections(id);
connectionsNotifier.value = connectionsNotifier.value connectionsNotifier.value = connectionsNotifier.value.copyWith(
.copyWith(connections: clashCore.getConnections()); connections: clashCore.getConnections(),
);
} }
@override @override
@@ -417,7 +310,12 @@ class ConnectionsSearchDelegate extends SearchDelegate {
key: Key(connection.id), key: Key(connection.id),
connection: connection, connection: connection,
onClick: _addKeyword, onClick: _addKeyword,
onBlock: _handleBlockConnection, trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
); );
}, },
separatorBuilder: (BuildContext context, int index) { separatorBuilder: (BuildContext context, int index) {

View File

@@ -1,4 +1,4 @@
export 'proxies.dart'; export 'proxies/proxies.dart';
export 'dashboard/dashboard.dart'; export 'dashboard/dashboard.dart';
export 'tools.dart'; export 'tools.dart';
export 'profiles/profiles.dart'; export 'profiles/profiles.dart';

View File

@@ -72,9 +72,6 @@ class _LogsFragmentState extends State<LogsFragment> {
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
), ),
const SizedBox(
width: 8,
)
]; ];
}); });
} }

View File

@@ -72,9 +72,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}, },
icon: const Icon(Icons.sync), icon: const Icon(Icons.sync),
), ),
const SizedBox(
width: 8,
)
]; ];
}, },
); );
@@ -145,12 +142,8 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: NotificationListener<ScrollNotification>( child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) { onNotification: (scrollNotification) {
WidgetsBinding.instance.addPostFrameCallback( hasPadding.value =
(_) { scrollNotification.metrics.maxScrollExtent > 0;
hasPadding.value =
scrollNotification.metrics.maxScrollExtent > 0;
},
);
return true; return true;
}, },
child: ValueListenableBuilder( child: ValueListenableBuilder(
@@ -161,7 +154,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
left: 16, left: 16,
right: 16, right: 16,
top: 16, top: 16,
bottom: 16 + (hasPadding ? 56 : 0), bottom: 16 + (hasPadding ? 72 : 0),
), ),
child: Grid( child: Grid(
mainAxisSpacing: 16, mainAxisSpacing: 16,
@@ -272,11 +265,13 @@ class _ProfileItemState extends State<ProfileItem> {
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Flexible(
profile.label ?? profile.id, child: Text(
style: textTheme.titleMedium, profile.label ?? profile.id,
maxLines: 1, style: textTheme.titleMedium,
overflow: TextOverflow.ellipsis, maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
Text( Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '', profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',

View File

@@ -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<ProxiesFragment> createState() => _ProxiesFragmentState();
}
class _ProxiesFragmentState extends State<ProxiesFragment> {
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
final items = [
CommonPopupMenuItem(
action: ProxiesSortType.none,
label: appLocalizations.defaultSort,
iconData: Icons.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<Config, ProxiesType>(
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<Config, ProxiesSortType>(
selector: (_, config) => config.proxiesSortType,
builder: (_, proxiesSortType, __) {
return CommonPopupMenu<ProxiesSortType>.radio(
items: items,
icon: const Icon(Icons.sort_sharp),
onSelected: (value) {
final config = context.read<Config>();
config.proxiesSortType = value;
},
selectedValue: proxiesSortType,
);
},
),
const SizedBox(
width: 8,
)
];
});
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
},
child: Selector<Config, ProxiesType>(
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<ProxiesTabFragment> createState() => _ProxiesTabFragmentState();
}
class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
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<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
shouldRebuild: (prev, next) {
if (!const ListEquality<String>()
.equals(prev.groupNames, next.groupNames)) {
_tabController?.removeListener(_handleTabControllerChange);
_tabController?.dispose();
_tabController = null;
return true;
}
return false;
},
builder: (_, state, __) {
final index = state.groupNames.indexWhere(
(item) => item == state.currentGroupName,
);
_tabController ??= TabController(
length: state.groupNames.length,
initialIndex: index == -1 ? 0 : index,
vsync: this,
)..addListener(_handleTabControllerChange);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
controller: _tabController,
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<ProxiesExpansionPanelFragment> createState() =>
_ProxiesExpansionPanelFragmentState();
}
class _ProxiesExpansionPanelFragmentState
extends State<ProxiesExpansionPanelFragment> {
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
builder: (_, state, __) {
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: state.groupNames.length,
itemBuilder: (_, index) {
final groupName = state.groupNames[index];
return 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<ProxyGroupView> createState() => _ProxyGroupViewState();
}
class _ProxyGroupViewState extends State<ProxyGroupView> {
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<Proxy> 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<AppState, Config, String>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName)!;
return config.currentSelectedMap[groupName] ?? group.now ?? '';
},
builder: (_, value, ___) {
return builder(value);
},
);
}
Widget _buildTabGroupView({
required List<Proxy> 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<Proxy> 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<Config, Set<String>>(
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<String>.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<AppState, Config, ProxyGroupSelectorState>(
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<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
}
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _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<double>(
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<AppState, int?>(
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<AppState, String>(
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,
],
),
)
],
),
),
);
}
}

View File

@@ -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<AppState, int?>(
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<AppState, String>(
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,
],
),
),
],
),
),
);
}
}

View File

@@ -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<ProxyGroupView> createState() => _ProxyGroupViewState();
}
class _ProxyGroupViewState extends State<ProxyGroupView> {
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<Proxy> 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<AppState, Config, String>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName)!;
return config.currentSelectedMap[groupName] ?? group.now ?? '';
},
builder: (_, value, ___) {
return builder(value);
},
);
}
Widget _buildTabGroupView({
required List<Proxy> 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<Proxy> 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<Config, Set<String>>(
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<String>.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<AppState, Config, ProxyGroupSelectorState>(
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<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
}
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _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<double>(
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,
);
}
}

View File

@@ -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<ProxiesListFragment> createState() =>
_ProxiesListFragmentState();
}
class _ProxiesListFragmentState
extends State<ProxiesListFragment> {
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
builder: (_, state, __) {
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: state.groupNames.length,
itemBuilder: (_, index) {
final groupName = state.groupNames[index];
return ProxyGroupView(
key: PageStorageKey(groupName),
groupName: groupName,
type: ProxiesType.list,
);
},
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(
height: 16,
);
},
);
},
);
}
}

View File

@@ -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<ProxiesFragment> createState() => _ProxiesFragmentState();
}
class _ProxiesFragmentState extends State<ProxiesFragment> {
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
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<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
},
child: Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType,
builder: (_, proxiesType, __) {
return switch (proxiesType) {
ProxiesType.tab => const ProxiesTabFragment(),
ProxiesType.list => const ProxiesListFragment(),
};
},
),
);
}
}

View File

@@ -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<Widget> _buildStyleSetting() {
return generateSection(
title: appLocalizations.style,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesType>(
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<Widget> _buildSortSetting() {
return generateSection(
title: appLocalizations.sort,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesSortType>(
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<Widget> _buildSizeSetting() {
return generateSection(
title: appLocalizations.size,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxyCardType>(
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<Widget> _buildColumnsSetting() {
return generateSection(
title: appLocalizations.columns,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
scrollDirection: Axis.horizontal,
child: Selector2<AppState, Config, ColumnsSelectorState>(
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,
),
),
);
}
}

View File

@@ -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<ProxiesTabFragment> createState() => _ProxiesTabFragmentState();
}
class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
with TickerProviderStateMixin {
TabController? _tabController;
final hasMoreButtonNotifier = ValueNotifier<bool>(false);
@override
void dispose() {
super.dispose();
_tabController?.dispose();
}
_buildMoreButton() {
return Selector<AppState, bool>(
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<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
builder: (_, state, __) {
return 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<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
shouldRebuild: (prev, next) {
if (!const ListEquality<String>()
.equals(prev.groupNames, next.groupNames)) {
_tabController?.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<ScrollMetricsNotification>(
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,
),
),
],
),
)
],
);
},
);
}
}

View File

@@ -1,11 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.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/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -68,9 +66,6 @@ class _RequestsFragmentState extends State<RequestsFragment> {
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
), ),
const SizedBox(
width: 8,
)
]; ];
}, },
); );
@@ -156,7 +151,7 @@ class _RequestsFragmentState extends State<RequestsFragment> {
controller: _scrollController, controller: _scrollController,
itemBuilder: (_, index) { itemBuilder: (_, index) {
final connection = connections[index]; final connection = connections[index];
return RequestItem( return ConnectionItem(
key: Key(connection.id), key: Key(connection.id),
connection: connection, connection: connection,
onClick: _addKeyword, onClick: _addKeyword,
@@ -178,104 +173,6 @@ class _RequestsFragmentState extends State<RequestsFragment> {
} }
} }
class RequestItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClick;
const RequestItem({
super.key,
required this.connection,
this.onClick,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
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<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
)
: null,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
),
);
}
}
class RequestsSearchDelegate extends SearchDelegate { class RequestsSearchDelegate extends SearchDelegate {
ValueNotifier<ConnectionsAndKeywords> requestsNotifier; ValueNotifier<ConnectionsAndKeywords> requestsNotifier;
@@ -394,7 +291,7 @@ class RequestsSearchDelegate extends SearchDelegate {
child: ListView.separated( child: ListView.separated(
itemBuilder: (_, index) { itemBuilder: (_, index) {
final connection = _results[index]; final connection = _results[index];
return RequestItem( return ConnectionItem(
key: Key(connection.id), key: Key(connection.id),
connection: connection, connection: connection,
onClick: (value) { onClick: (value) {

View File

@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -21,8 +23,95 @@ class ThemeModeItem {
class ThemeFragment extends StatelessWidget { class ThemeFragment extends StatelessWidget {
const ThemeFragment({super.key}); const ThemeFragment({super.key});
Widget _themeModeCheckBox({ Widget _itemCard({
required BuildContext context, 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<ThemeColorsBox> createState() => _ThemeColorsBoxState();
}
class _ThemeColorsBoxState extends State<ThemeColorsBox> {
Widget _themeModeCheckBox({
bool? isSelected, bool? isSelected,
required ThemeModeItem themeModeItem, required ThemeModeItem themeModeItem,
}) { }) {
@@ -32,7 +121,7 @@ class ThemeFragment extends StatelessWidget {
globalState.appController.config.themeMode = themeModeItem.themeMode; globalState.appController.config.themeMode = themeModeItem.themeMode;
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal:16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@@ -55,7 +144,6 @@ class ThemeFragment extends StatelessWidget {
} }
Widget _primaryColorCheckBox({ Widget _primaryColorCheckBox({
required BuildContext context,
bool? isSelected, bool? isSelected,
Color? color, Color? color,
}) { }) {
@@ -68,28 +156,8 @@ class ThemeFragment extends StatelessWidget {
); );
} }
Widget _itemCard({ @override
required BuildContext context, Widget build(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) {
List<ThemeModeItem> themeModeItems = [ List<ThemeModeItem> themeModeItems = [
ThemeModeItem( ThemeModeItem(
iconData: Icons.auto_mode, iconData: Icons.auto_mode,
@@ -118,8 +186,7 @@ class ThemeFragment extends StatelessWidget {
]; ];
return Column( return Column(
children: [ children: [
_itemCard( ItemCard(
context: context,
info: Info( info: Info(
label: appLocalizations.themeMode, label: appLocalizations.themeMode,
iconData: Icons.brightness_high, iconData: Icons.brightness_high,
@@ -137,7 +204,6 @@ class ThemeFragment extends StatelessWidget {
final themeModeItem = themeModeItems[index]; final themeModeItem = themeModeItems[index];
return _themeModeCheckBox( return _themeModeCheckBox(
isSelected: themeMode == themeModeItem.themeMode, isSelected: themeMode == themeModeItem.themeMode,
context: context,
themeModeItem: themeModeItem, themeModeItem: themeModeItem,
); );
}, },
@@ -151,8 +217,7 @@ class ThemeFragment extends StatelessWidget {
}, },
), ),
), ),
_itemCard( ItemCard(
context: context,
info: Info( info: Info(
label: appLocalizations.themeColor, label: appLocalizations.themeColor,
iconData: Icons.palette, iconData: Icons.palette,
@@ -172,7 +237,6 @@ class ThemeFragment extends StatelessWidget {
itemBuilder: (_, index) { itemBuilder: (_, index) {
final primaryColor = primaryColors[index]; final primaryColor = primaryColors[index];
return _primaryColorCheckBox( return _primaryColorCheckBox(
context: context,
isSelected: currentPrimaryColor == primaryColor?.value, isSelected: currentPrimaryColor == primaryColor?.value,
color: primaryColor, 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,
],
),
);
}
} }

View File

@@ -195,5 +195,21 @@
"testUrl": "Test url", "testUrl": "Test url",
"sync": "Sync", "sync": "Sync",
"exclude": "Hidden from recent tasks", "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"
} }

View File

@@ -195,5 +195,21 @@
"testUrl": "测速链接", "testUrl": "测速链接",
"sync": "同步", "sync": "同步",
"exclude": "从最近任务中隐藏", "exclude": "从最近任务中隐藏",
"excludeDesc": "应用在后台时,从最近任务中隐藏应用" "excludeDesc": "应用在后台时,从最近任务中隐藏应用",
"oneColumn": "一列",
"twoColumns": "两列",
"threeColumns": "三列",
"fourColumns": "四列",
"expand": "标准",
"shrink": "紧凑",
"min": "最小",
"tab": "标签页",
"list": "列表",
"delay": "延迟",
"style": "风格",
"size": "尺寸",
"sort": "排序",
"columns": "列数",
"proxiesSetting": "代理设置",
"proxyGroup": "代理组"
} }

View File

@@ -87,6 +87,7 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdateError": MessageLookupByLibrary.simpleMessage( "checkUpdateError": MessageLookupByLibrary.simpleMessage(
"The current application is already the latest version"), "The current application is already the latest version"),
"checking": MessageLookupByLibrary.simpleMessage("Checking..."), "checking": MessageLookupByLibrary.simpleMessage("Checking..."),
"columns": MessageLookupByLibrary.simpleMessage("Columns"),
"compatible": "compatible":
MessageLookupByLibrary.simpleMessage("Compatibility mode"), MessageLookupByLibrary.simpleMessage("Compatibility mode"),
"compatibleDesc": MessageLookupByLibrary.simpleMessage( "compatibleDesc": MessageLookupByLibrary.simpleMessage(
@@ -107,6 +108,7 @@ class MessageLookup extends MessageLookupByLibrary {
"days": MessageLookupByLibrary.simpleMessage("Days"), "days": MessageLookupByLibrary.simpleMessage("Days"),
"defaultSort": MessageLookupByLibrary.simpleMessage("Sort by default"), "defaultSort": MessageLookupByLibrary.simpleMessage("Sort by default"),
"defaultText": MessageLookupByLibrary.simpleMessage("Default"), "defaultText": MessageLookupByLibrary.simpleMessage("Default"),
"delay": MessageLookupByLibrary.simpleMessage("Delay"),
"delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"), "delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"),
"delete": MessageLookupByLibrary.simpleMessage("Delete"), "delete": MessageLookupByLibrary.simpleMessage("Delete"),
"desc": MessageLookupByLibrary.simpleMessage( "desc": MessageLookupByLibrary.simpleMessage(
@@ -126,6 +128,7 @@ class MessageLookup extends MessageLookupByLibrary {
"excludeDesc": MessageLookupByLibrary.simpleMessage( "excludeDesc": MessageLookupByLibrary.simpleMessage(
"When the app is in the background, the app is hidden from the recent task"), "When the app is in the background, the app is hidden from the recent task"),
"exit": MessageLookupByLibrary.simpleMessage("Exit"), "exit": MessageLookupByLibrary.simpleMessage("Exit"),
"expand": MessageLookupByLibrary.simpleMessage("Standard"),
"expirationTime": "expirationTime":
MessageLookupByLibrary.simpleMessage("Expiration time"), MessageLookupByLibrary.simpleMessage("Expiration time"),
"externalController": "externalController":
@@ -142,6 +145,7 @@ class MessageLookup extends MessageLookupByLibrary {
"findProcessMode": MessageLookupByLibrary.simpleMessage("Find process"), "findProcessMode": MessageLookupByLibrary.simpleMessage("Find process"),
"findProcessModeDesc": MessageLookupByLibrary.simpleMessage( "findProcessModeDesc": MessageLookupByLibrary.simpleMessage(
"There is a risk of flashback after opening"), "There is a risk of flashback after opening"),
"fourColumns": MessageLookupByLibrary.simpleMessage("Four columns"),
"general": MessageLookupByLibrary.simpleMessage("General"), "general": MessageLookupByLibrary.simpleMessage("General"),
"geoData": MessageLookupByLibrary.simpleMessage("GeoData"), "geoData": MessageLookupByLibrary.simpleMessage("GeoData"),
"geodataLoader": "geodataLoader":
@@ -162,12 +166,14 @@ class MessageLookup extends MessageLookupByLibrary {
"just": MessageLookupByLibrary.simpleMessage("Just"), "just": MessageLookupByLibrary.simpleMessage("Just"),
"language": MessageLookupByLibrary.simpleMessage("Language"), "language": MessageLookupByLibrary.simpleMessage("Language"),
"light": MessageLookupByLibrary.simpleMessage("Light"), "light": MessageLookupByLibrary.simpleMessage("Light"),
"list": MessageLookupByLibrary.simpleMessage("List"),
"logLevel": MessageLookupByLibrary.simpleMessage("LogLevel"), "logLevel": MessageLookupByLibrary.simpleMessage("LogLevel"),
"logcat": MessageLookupByLibrary.simpleMessage("Logcat"), "logcat": MessageLookupByLibrary.simpleMessage("Logcat"),
"logcatDesc": MessageLookupByLibrary.simpleMessage( "logcatDesc": MessageLookupByLibrary.simpleMessage(
"Disabling will hide the log entry"), "Disabling will hide the log entry"),
"logs": MessageLookupByLibrary.simpleMessage("Logs"), "logs": MessageLookupByLibrary.simpleMessage("Logs"),
"logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"), "logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"),
"min": MessageLookupByLibrary.simpleMessage("Min"),
"minimizeOnExit": "minimizeOnExit":
MessageLookupByLibrary.simpleMessage("Minimize on exit"), MessageLookupByLibrary.simpleMessage("Minimize on exit"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage(
@@ -195,6 +201,7 @@ class MessageLookup extends MessageLookupByLibrary {
"nullProfileDesc": MessageLookupByLibrary.simpleMessage( "nullProfileDesc": MessageLookupByLibrary.simpleMessage(
"No profile, Please add a profile"), "No profile, Please add a profile"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"),
"other": MessageLookupByLibrary.simpleMessage("Other"), "other": MessageLookupByLibrary.simpleMessage("Other"),
"outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"), "outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"),
"override": MessageLookupByLibrary.simpleMessage("Override"), "override": MessageLookupByLibrary.simpleMessage("Override"),
@@ -230,6 +237,9 @@ class MessageLookup extends MessageLookupByLibrary {
"profiles": MessageLookupByLibrary.simpleMessage("Profiles"), "profiles": MessageLookupByLibrary.simpleMessage("Profiles"),
"project": MessageLookupByLibrary.simpleMessage("Project"), "project": MessageLookupByLibrary.simpleMessage("Project"),
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"), "proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
"proxiesSetting":
MessageLookupByLibrary.simpleMessage("Proxies setting"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("Proxy group"),
"proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"), "proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage( "proxyPortDesc": MessageLookupByLibrary.simpleMessage(
"Set the Clash listening port"), "Set the Clash listening port"),
@@ -258,16 +268,21 @@ class MessageLookup extends MessageLookupByLibrary {
"selected": MessageLookupByLibrary.simpleMessage("Selected"), "selected": MessageLookupByLibrary.simpleMessage("Selected"),
"settings": MessageLookupByLibrary.simpleMessage("Settings"), "settings": MessageLookupByLibrary.simpleMessage("Settings"),
"show": MessageLookupByLibrary.simpleMessage("Show"), "show": MessageLookupByLibrary.simpleMessage("Show"),
"shrink": MessageLookupByLibrary.simpleMessage("Shrink"),
"silentLaunch": MessageLookupByLibrary.simpleMessage("SilentLaunch"), "silentLaunch": MessageLookupByLibrary.simpleMessage("SilentLaunch"),
"silentLaunchDesc": "silentLaunchDesc":
MessageLookupByLibrary.simpleMessage("Start in the background"), MessageLookupByLibrary.simpleMessage("Start in the background"),
"size": MessageLookupByLibrary.simpleMessage("Size"),
"sort": MessageLookupByLibrary.simpleMessage("Sort"),
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."), "startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"style": MessageLookupByLibrary.simpleMessage("Style"),
"submit": MessageLookupByLibrary.simpleMessage("Submit"), "submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"), "sync": MessageLookupByLibrary.simpleMessage("Sync"),
"systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"), "systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage( "systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"), "Attach HTTP proxy to VpnService"),
"tab": MessageLookupByLibrary.simpleMessage("Tab"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("Tab animation"), "tabAnimation": MessageLookupByLibrary.simpleMessage("Tab animation"),
"tabAnimationDesc": MessageLookupByLibrary.simpleMessage( "tabAnimationDesc": MessageLookupByLibrary.simpleMessage(
"When enabled, the home tab will add a toggle animation"), "When enabled, the home tab will add a toggle animation"),
@@ -280,12 +295,14 @@ class MessageLookup extends MessageLookupByLibrary {
"themeDesc": MessageLookupByLibrary.simpleMessage( "themeDesc": MessageLookupByLibrary.simpleMessage(
"Set dark mode,adjust the color"), "Set dark mode,adjust the color"),
"themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"), "themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"),
"threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"),
"tip": MessageLookupByLibrary.simpleMessage("tip"), "tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"), "tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("TUN mode"), "tun": MessageLookupByLibrary.simpleMessage("TUN mode"),
"tunDesc": MessageLookupByLibrary.simpleMessage( "tunDesc": MessageLookupByLibrary.simpleMessage(
"only effective in administrator mode"), "only effective in administrator mode"),
"twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"),
"unableToUpdateCurrentProfileDesc": "unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage(
"unable to update current profile"), "unable to update current profile"),

View File

@@ -71,6 +71,7 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."), "checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"columns": MessageLookupByLibrary.simpleMessage("列数"),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"), "compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
"compatibleDesc": "compatibleDesc":
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"), MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"),
@@ -89,6 +90,7 @@ class MessageLookup extends MessageLookupByLibrary {
"days": MessageLookupByLibrary.simpleMessage(""), "days": MessageLookupByLibrary.simpleMessage(""),
"defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"), "defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"),
"defaultText": MessageLookupByLibrary.simpleMessage("默认"), "defaultText": MessageLookupByLibrary.simpleMessage("默认"),
"delay": MessageLookupByLibrary.simpleMessage("延迟"),
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"), "delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
"delete": MessageLookupByLibrary.simpleMessage("删除"), "delete": MessageLookupByLibrary.simpleMessage("删除"),
"desc": MessageLookupByLibrary.simpleMessage( "desc": MessageLookupByLibrary.simpleMessage(
@@ -104,6 +106,7 @@ class MessageLookup extends MessageLookupByLibrary {
"excludeDesc": "excludeDesc":
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"), MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
"exit": MessageLookupByLibrary.simpleMessage("退出"), "exit": MessageLookupByLibrary.simpleMessage("退出"),
"expand": MessageLookupByLibrary.simpleMessage("标准"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"), "expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"), "externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc": "externalControllerDesc":
@@ -115,6 +118,7 @@ class MessageLookup extends MessageLookupByLibrary {
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"), "findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
"findProcessModeDesc": "findProcessModeDesc":
MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"), MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
"general": MessageLookupByLibrary.simpleMessage("基础"), "general": MessageLookupByLibrary.simpleMessage("基础"),
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"), "geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"), "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
@@ -131,11 +135,13 @@ class MessageLookup extends MessageLookupByLibrary {
"just": MessageLookupByLibrary.simpleMessage("刚刚"), "just": MessageLookupByLibrary.simpleMessage("刚刚"),
"language": MessageLookupByLibrary.simpleMessage("语言"), "language": MessageLookupByLibrary.simpleMessage("语言"),
"light": MessageLookupByLibrary.simpleMessage("浅色"), "light": MessageLookupByLibrary.simpleMessage("浅色"),
"list": MessageLookupByLibrary.simpleMessage("列表"),
"logLevel": MessageLookupByLibrary.simpleMessage("日志等级"), "logLevel": MessageLookupByLibrary.simpleMessage("日志等级"),
"logcat": MessageLookupByLibrary.simpleMessage("日志捕获"), "logcat": MessageLookupByLibrary.simpleMessage("日志捕获"),
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"logs": MessageLookupByLibrary.simpleMessage("日志"), "logs": MessageLookupByLibrary.simpleMessage("日志"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"), "logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"minimizeOnExitDesc": "minimizeOnExitDesc":
MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
@@ -158,6 +164,7 @@ class MessageLookup extends MessageLookupByLibrary {
"nullProfileDesc": "nullProfileDesc":
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
"other": MessageLookupByLibrary.simpleMessage("其他"), "other": MessageLookupByLibrary.simpleMessage("其他"),
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
"override": MessageLookupByLibrary.simpleMessage("覆写"), "override": MessageLookupByLibrary.simpleMessage("覆写"),
@@ -187,6 +194,8 @@ class MessageLookup extends MessageLookupByLibrary {
"profiles": MessageLookupByLibrary.simpleMessage("配置"), "profiles": MessageLookupByLibrary.simpleMessage("配置"),
"project": MessageLookupByLibrary.simpleMessage("项目"), "project": MessageLookupByLibrary.simpleMessage("项目"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"), "proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
@@ -207,15 +216,20 @@ class MessageLookup extends MessageLookupByLibrary {
"selected": MessageLookupByLibrary.simpleMessage("已选择"), "selected": MessageLookupByLibrary.simpleMessage("已选择"),
"settings": MessageLookupByLibrary.simpleMessage("设置"), "settings": MessageLookupByLibrary.simpleMessage("设置"),
"show": MessageLookupByLibrary.simpleMessage("显示"), "show": MessageLookupByLibrary.simpleMessage("显示"),
"shrink": MessageLookupByLibrary.simpleMessage("紧凑"),
"silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"), "silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"),
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
"size": MessageLookupByLibrary.simpleMessage("尺寸"),
"sort": MessageLookupByLibrary.simpleMessage("排序"),
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"style": MessageLookupByLibrary.simpleMessage("风格"),
"submit": MessageLookupByLibrary.simpleMessage("提交"), "submit": MessageLookupByLibrary.simpleMessage("提交"),
"sync": MessageLookupByLibrary.simpleMessage("同步"), "sync": MessageLookupByLibrary.simpleMessage("同步"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"), "systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc": "systemProxyDesc":
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"), MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
"tab": MessageLookupByLibrary.simpleMessage("标签页"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"), "tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
"tabAnimationDesc": "tabAnimationDesc":
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"), MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"),
@@ -226,11 +240,13 @@ class MessageLookup extends MessageLookupByLibrary {
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"), "themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
"tip": MessageLookupByLibrary.simpleMessage("提示"), "tip": MessageLookupByLibrary.simpleMessage("提示"),
"tools": MessageLookupByLibrary.simpleMessage("工具"), "tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("TUN模式"), "tun": MessageLookupByLibrary.simpleMessage("TUN模式"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"), "tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
"unableToUpdateCurrentProfileDesc": "unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"), MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"),

View File

@@ -2019,6 +2019,166 @@ class AppLocalizations {
args: [], 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<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -15,12 +15,12 @@ import 'common/common.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await android?.init();
await window?.init();
clashCore.initMessage(); clashCore.initMessage();
globalState.packageInfo = await PackageInfo.fromPlatform(); globalState.packageInfo = await PackageInfo.fromPlatform();
final config = await preferences.getConfig() ?? Config(); final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await android?.init();
await window?.init(config.windowProps);
final appState = AppState( final appState = AppState(
mode: clashConfig.mode, mode: clashConfig.mode,
isCompatible: config.isCompatible, isCompatible: config.isCompatible,
@@ -67,7 +67,6 @@ Future<void> vpnService() async {
clashCore.setFdMap(fd.id); clashCore.setFdMap(fd.id);
}, },
onProcess: (Process process) async { onProcess: (Process process) async {
print(process);
var packageName = await app?.resolverProcess(process); var packageName = await app?.resolverProcess(process);
clashCore.setProcessMap( clashCore.setProcessMap(
ProcessMapItem( ProcessMapItem(
@@ -112,6 +111,7 @@ Future<void> vpnService() async {
onStop: () async { onStop: () async {
await app?.tip(appLocalizations.stopVpn); await app?.tip(appLocalizations.stopVpn);
await globalState.stopSystemProxy(); await globalState.stopSystemProxy();
clashCore.shutdown();
exit(0); exit(0);
}, },
), ),
@@ -119,7 +119,7 @@ Future<void> vpnService() async {
globalState.updateTraffic(); globalState.updateTraffic();
globalState.updateFunctionLists = [ globalState.updateFunctionLists = [
() { () {
globalState.updateTraffic(); globalState.updateTraffic();
} }
]; ];
@@ -137,7 +137,8 @@ class ServiceMessageHandler with ServiceMessageListener {
required Function(Process process) onProcess, required Function(Process process) onProcess,
required Function(String runTime) onStarted, required Function(String runTime) onStarted,
required Function(String groupName) onLoaded, required Function(String groupName) onLoaded,
}) : _onProtect = onProtect, })
: _onProtect = onProtect,
_onProcess = onProcess, _onProcess = onProcess,
_onStarted = onStarted, _onStarted = onStarted,
_onLoaded = onLoaded; _onLoaded = onLoaded;
@@ -163,10 +164,11 @@ class ServiceMessageHandler with ServiceMessageListener {
} }
} }
@immutable
class TileListenerWithVpn with TileListener { class TileListenerWithVpn with TileListener {
final Function() _onStop; final Function() _onStop;
TileListenerWithVpn({ const TileListenerWithVpn({
required Function() onStop, required Function() onStop,
}) : _onStop = onStop; }) : _onStop = onStop;

View File

@@ -311,6 +311,13 @@ class ClashConfig extends ChangeNotifier {
_mode = clashConfig._mode; _mode = clashConfig._mode;
_logLevel = clashConfig._logLevel; _logLevel = clashConfig._logLevel;
_tun = clashConfig._tun; _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; _dns = clashConfig._dns;
_rules = clashConfig._rules; _rules = clashConfig._rules;
_globalRealUa = clashConfig.globalRealUa; _globalRealUa = clashConfig.globalRealUa;

View File

@@ -36,6 +36,21 @@ class Props with _$Props {
factory Props.fromJson(Map<String, Object?> json) => _$PropsFromJson(json); factory Props.fromJson(Map<String, Object?> 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<String, Object?>? json) =>
json == null ? defaultWindowProps : _$WindowPropsFromJson(json);
}
const defaultWindowProps = WindowProps();
@JsonSerializable() @JsonSerializable()
class Config extends ChangeNotifier { class Config extends ChangeNotifier {
List<Profile> _profiles; List<Profile> _profiles;
@@ -62,6 +77,7 @@ class Config extends ChangeNotifier {
ProxyCardType _proxyCardType; ProxyCardType _proxyCardType;
int _proxiesColumns; int _proxiesColumns;
String _testUrl; String _testUrl;
WindowProps _windowProps;
Config() Config()
: _profiles = [], : _profiles = [],
@@ -83,6 +99,7 @@ class Config extends ChangeNotifier {
_allowBypass = true, _allowBypass = true,
_isExclude = false, _isExclude = false,
_proxyCardType = ProxyCardType.expand, _proxyCardType = ProxyCardType.expand,
_windowProps = defaultWindowProps,
_proxiesType = ProxiesType.tab, _proxiesType = ProxiesType.tab,
_proxiesColumns = 2; _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; ProxiesType get proxiesType => _proxiesType;
set proxiesType(ProxiesType value) { 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([ update([
Config? config, Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all, RecoveryOption recoveryOptions = RecoveryOption.all,
@@ -470,7 +499,9 @@ class Config extends ChangeNotifier {
_isAnimateToPage = config._isAnimateToPage; _isAnimateToPage = config._isAnimateToPage;
_autoCheckUpdate = config._autoCheckUpdate; _autoCheckUpdate = config._autoCheckUpdate;
_dav = config._dav; _dav = config._dav;
_testUrl = config.testUrl; _testUrl = config._testUrl;
_isExclude = config._isExclude;
_windowProps = config._windowProps;
} }
notifyListeners(); notifyListeners();
} }
@@ -482,9 +513,4 @@ class Config extends ChangeNotifier {
factory Config.fromJson(Map<String, dynamic> json) { factory Config.fromJson(Map<String, dynamic> json) {
return _$ConfigFromJson(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}';
}
} }

View File

@@ -48,7 +48,11 @@ class ConnectionsAndKeywords with _$ConnectionsAndKeywords {
_$ConnectionsAndKeywordsFromJson(json); _$ConnectionsAndKeywordsFromJson(json);
} }
extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords {
extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords{ List<Connection> get filteredConnections => connections
List<Connection> get filteredConnections => connections.where((connection)=> Set.from(connection.chains).containsAll(keywords)).toList(); .where((connection) => {
} ...connection.chains,
connection.metadata.process,
}.containsAll(keywords))
.toList();
}

View File

@@ -429,3 +429,194 @@ abstract class _Props implements Props {
_$$PropsImplCopyWith<_$PropsImpl> get copyWith => _$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
WindowProps _$WindowPropsFromJson(Map<String, dynamic> 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<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$WindowPropsCopyWith<WindowProps> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
}

View File

@@ -35,16 +35,18 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true ..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
..allowBypass = json['allowBypass'] as bool? ?? true ..allowBypass = json['allowBypass'] as bool? ?? true
..systemProxy = json['systemProxy'] as bool? ?? true ..systemProxy = json['systemProxy'] as bool? ?? true
..proxiesType = ..proxiesType = $enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType'],
$enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType']) ?? unknownValue: ProxiesType.tab) ??
ProxiesType.tab ProxiesType.tab
..proxyCardType = ..proxyCardType =
$enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ?? $enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ??
ProxyCardType.expand ProxyCardType.expand
..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2 ..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2
..testUrl = ..testUrl =
json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204' 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<String, dynamic>?);
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles, 'profiles': instance.profiles,
@@ -71,6 +73,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'proxiesColumns': instance.proxiesColumns, 'proxiesColumns': instance.proxiesColumns,
'test-url': instance.testUrl, 'test-url': instance.testUrl,
'isExclude': instance.isExclude, 'isExclude': instance.isExclude,
'windowProps': instance.windowProps,
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {
@@ -87,12 +90,13 @@ const _$ProxiesSortTypeEnumMap = {
const _$ProxiesTypeEnumMap = { const _$ProxiesTypeEnumMap = {
ProxiesType.tab: 'tab', ProxiesType.tab: 'tab',
ProxiesType.expansion: 'expansion', ProxiesType.list: 'list',
}; };
const _$ProxyCardTypeEnumMap = { const _$ProxyCardTypeEnumMap = {
ProxyCardType.expand: 'expand', ProxyCardType.expand: 'expand',
ProxyCardType.shrink: 'shrink', ProxyCardType.shrink: 'shrink',
ProxyCardType.min: 'min',
}; };
_$AccessControlImpl _$$AccessControlImplFromJson(Map<String, dynamic> json) => _$AccessControlImpl _$$AccessControlImplFromJson(Map<String, dynamic> json) =>
@@ -138,3 +142,19 @@ Map<String, dynamic> _$$PropsImplToJson(_$PropsImpl instance) =>
'allowBypass': instance.allowBypass, 'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy, 'systemProxy': instance.systemProxy,
}; };
_$WindowPropsImpl _$$WindowPropsImplFromJson(Map<String, dynamic> 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<String, dynamic> _$$WindowPropsImplToJson(_$WindowPropsImpl instance) =>
<String, dynamic>{
'width': instance.width,
'height': instance.height,
'top': instance.top,
'left': instance.left,
};

View File

@@ -2261,3 +2261,143 @@ abstract class _PackageListSelectorState implements PackageListSelectorState {
_$$PackageListSelectorStateImplCopyWith<_$PackageListSelectorStateImpl> _$$PackageListSelectorStateImplCopyWith<_$PackageListSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
/// @nodoc
mixin _$ColumnsSelectorState {
int get columns => throw _privateConstructorUsedError;
ViewMode get viewMode => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ColumnsSelectorStateCopyWith<ColumnsSelectorState> 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;
}

View File

@@ -124,3 +124,12 @@ class PackageListSelectorState with _$PackageListSelectorState {
required bool isAccessControl, required bool isAccessControl,
}) = _PackageListSelectorState; }) = _PackageListSelectorState;
} }
@freezed
class ColumnsSelectorState with _$ColumnsSelectorState {
const factory ColumnsSelectorState({
required int columns,
required ViewMode viewMode,
}) = _ColumnsSelectorState;
}

View File

@@ -14,6 +14,7 @@ class HomePage extends StatelessWidget {
const HomePage({super.key}); const HomePage({super.key});
_getNavigationBar({ _getNavigationBar({
required BuildContext context,
required ViewMode viewMode, required ViewMode viewMode,
required List<NavigationItem> navigationItems, required List<NavigationItem> navigationItems,
required int currentIndex, required int currentIndex,
@@ -34,6 +35,8 @@ class HomePage extends StatelessWidget {
} }
final extended = viewMode == ViewMode.desktop; final extended = viewMode == ViewMode.desktop;
return NavigationRail( return NavigationRail(
backgroundColor: context.colorScheme.surfaceContainer,
groupAlignment: -0.8,
destinations: navigationItems destinations: navigationItems
.map( .map(
(e) => NavigationRailDestination( (e) => NavigationRailDestination(
@@ -82,32 +85,37 @@ class HomePage extends StatelessWidget {
); );
final currentIndex = index == -1 ? 0 : index; final currentIndex = index == -1 ? 0 : index;
final navigationBar = _getNavigationBar( final navigationBar = _getNavigationBar(
context: context,
viewMode: viewMode, viewMode: viewMode,
navigationItems: navigationItems, navigationItems: navigationItems,
currentIndex: currentIndex, currentIndex: currentIndex,
); );
final bottomNavigationBar = final bottomNavigationBar =
viewMode == ViewMode.mobile ? navigationBar : null; viewMode == ViewMode.mobile ? navigationBar : null;
Widget body;
if (viewMode != ViewMode.mobile) { if (viewMode != ViewMode.mobile) {
body = Row( return Row(
children: [ children: [
navigationBar, navigationBar,
Expanded( Expanded(
flex: 1, flex: 1,
child: child!, child: CommonScaffold(
key: globalState.homeScaffoldKey,
title: Intl.message(
currentLabel,
),
body: child!,
bottomNavigationBar: bottomNavigationBar,
),
) )
], ],
); );
} else {
body = child!;
} }
return CommonScaffold( return CommonScaffold(
key: globalState.homeScaffoldKey, key: globalState.homeScaffoldKey,
title: Intl.message( title: Intl.message(
currentLabel, currentLabel,
), ),
body: body, body: child!,
bottomNavigationBar: bottomNavigationBar, bottomNavigationBar: bottomNavigationBar,
); );
}, },

View File

@@ -8,6 +8,7 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:proxy/proxy_platform_interface.dart'; import 'package:proxy/proxy_platform_interface.dart';
@@ -80,6 +81,7 @@ class Proxy extends ProxyPlatform {
bool get isStart => startTime != null && startTime!.isBeforeNow; bool get isStart => startTime != null && startTime!.isBeforeNow;
onStarted(int? fd) { onStarted(int? fd) {
debugPrint("onStarted ==> $fd");
if (fd == null) return; if (fd == null) return;
if (receiver != null) { if (receiver != null) {
receiver!.close(); receiver!.close();

View File

@@ -92,7 +92,7 @@ class GlobalState {
appState: appState, appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
).then((_){ ).then((_) {
globalState.appController.addCheckIpNumDebounce(); globalState.appController.addCheckIpNumDebounce();
}); });
} }
@@ -252,18 +252,6 @@ class GlobalState {
return null; 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(); final globalState = GlobalState();

View File

@@ -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<ImageProvider?> _getPackageIcon(Connection connection) async {
return await app?.getPackageIcon(connection.metadata.process);
}
String _getRequestText(Metadata metadata) {
var text = "${metadata.network}://";
final ips = [
metadata.host,
metadata.destinationIP,
].where((ip) => ip.isNotEmpty);
text += ips.join("/");
text += ":${metadata.destinationPort}";
return text;
}
String _getSourceText(Connection connection) {
final metadata = connection.metadata;
if (metadata.process.isEmpty) {
return connection.start.lastUpdateTimeDesc;
}
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
}
@override
Widget build(BuildContext context) {
if (!Platform.isAndroid) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
),
trailing: trailing,
);
}
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, value, child) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: value
? GestureDetector(
onTap: () {
if (onClick == null) return;
final process = connection.metadata.process;
if(process.isEmpty) return;
onClick!(process);
},
child: Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
),
)
: null,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
),
trailing: trailing,
);
},
);
}
}

View File

@@ -5,7 +5,7 @@ import 'package:fl_clash/widgets/open_container.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'card.dart'; import 'card.dart';
import 'extend_page.dart'; import 'sheet.dart';
import 'scaffold.dart'; import 'scaffold.dart';
class Delegate { class Delegate {

View File

@@ -119,14 +119,21 @@ class CommonScaffoldState extends State<CommonScaffold> {
child: Stack( child: Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
ValueListenableBuilder( ValueListenableBuilder<List<Widget>>(
valueListenable: _actions, valueListenable: _actions,
builder: (_, actions, __) { builder: (_, actions, __) {
final realActions =
actions.isNotEmpty ? actions : widget.actions;
return AppBar( return AppBar(
automaticallyImplyLeading: widget.automaticallyImplyLeading, automaticallyImplyLeading: widget.automaticallyImplyLeading,
leading: widget.leading, leading: widget.leading,
title: Text(widget.title), title: Text(widget.title),
actions: actions.isNotEmpty ? actions : widget.actions, actions: [
...?realActions,
const SizedBox(
width: 8,
)
],
); );
}, },
), ),

View File

@@ -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,
);
}
}

View File

@@ -84,8 +84,11 @@ class _SideSheetState extends State<SideSheet> {
borderRadius: BorderRadius.circular(0), borderRadius: BorderRadius.circular(0),
); );
final BoxConstraints constraints = final BoxConstraints constraints = widget.constraints ??
widget.constraints ?? const BoxConstraints(maxWidth: 320); const BoxConstraints(
maxWidth: 320,
minWidth: 320
);
final Clip clipBehavior = widget.clipBehavior ?? Clip.none; final Clip clipBehavior = widget.clipBehavior ?? Clip.none;
@@ -403,27 +406,26 @@ class _ModalSideSheetState<T> extends State<_ModalSideSheet<T>> {
} }
class ModalSideSheetRoute<T> extends PopupRoute<T> { class ModalSideSheetRoute<T> extends PopupRoute<T> {
ModalSideSheetRoute({ ModalSideSheetRoute(
required this.builder, {required this.builder,
this.capturedThemes, this.capturedThemes,
this.barrierLabel, this.barrierLabel,
this.barrierOnTapHint, this.barrierOnTapHint,
this.backgroundColor, this.backgroundColor,
this.elevation, this.elevation,
this.shape, this.shape,
this.clipBehavior, this.clipBehavior,
this.constraints, this.constraints,
this.modalBarrierColor, this.modalBarrierColor,
this.isDismissible = true, this.isDismissible = true,
this.isScrollControlled = false, this.isScrollControlled = false,
this.scrollControlDisabledMaxHeightRatio = this.scrollControlDisabledMaxHeightRatio =
_defaultScrollControlDisabledMaxHeightRatio, _defaultScrollControlDisabledMaxHeightRatio,
super.settings, super.settings,
this.transitionAnimationController, this.transitionAnimationController,
this.anchorPoint, this.anchorPoint,
this.useSafeArea = false, this.useSafeArea = false,
super.filter super.filter});
});
final WidgetBuilder builder; final WidgetBuilder builder;
@@ -601,7 +603,9 @@ Future<T?> showModalSideSheet<T>({
width: kToolbarHeight, width: kToolbarHeight,
child: BackButton(), child: BackButton(),
), ),
const SizedBox(width: 8,), const SizedBox(
width: 8,
),
Expanded( Expanded(
child: Text( child: Text(
title, title,
@@ -617,6 +621,7 @@ Future<T?> showModalSideSheet<T>({
), ),
), ),
Expanded( Expanded(
flex: 1,
child: body, child: body,
), ),
], ],

View File

@@ -35,6 +35,9 @@ class _TrayContainerState extends State<TrayContainer> with TrayListener {
await trayManager.setIcon( await trayManager.setIcon(
other.getTrayIconPath(), other.getTrayIconPath(),
); );
await trayManager.setToolTip(
appName,
);
isTrayInit = true; isTrayInit = true;
} }
} }
@@ -44,6 +47,9 @@ class _TrayContainerState extends State<TrayContainer> with TrayListener {
await trayManager.setIcon( await trayManager.setIcon(
other.getTrayIconPath(), other.getTrayIconPath(),
); );
await trayManager.setToolTip(
appName,
);
} }
updateMenu(TrayContainerSelectorState state) async { updateMenu(TrayContainerSelectorState state) async {

View File

@@ -11,7 +11,7 @@ export 'null_status.dart';
export 'pop_container.dart'; export 'pop_container.dart';
export 'disabled_mask.dart'; export 'disabled_mask.dart';
export 'side_sheet.dart'; export 'side_sheet.dart';
export 'extend_page.dart'; export 'sheet.dart';
export 'keep_container.dart'; export 'keep_container.dart';
export 'animate_grid.dart'; export 'animate_grid.dart';
export 'tray_container.dart'; export 'tray_container.dart';
@@ -22,4 +22,5 @@ export 'tile_container.dart';
export 'chip.dart'; export 'chip.dart';
export 'fade_box.dart'; export 'fade_box.dart';
export 'app_state_container.dart'; export 'app_state_container.dart';
export 'text.dart'; export 'text.dart';
export 'connection_item.dart';

View File

@@ -18,7 +18,6 @@ class WindowContainer extends StatefulWidget {
} }
class _WindowContainerState extends State<WindowContainer> with WindowListener { class _WindowContainerState extends State<WindowContainer> with WindowListener {
_autoLaunchContainer(Widget child) { _autoLaunchContainer(Widget child) {
return Selector<Config, bool>( return Selector<Config, bool>(
selector: (_, config) => config.autoLaunch, selector: (_, config) => config.autoLaunch,
@@ -47,6 +46,28 @@ class _WindowContainerState extends State<WindowContainer> with WindowListener {
super.onWindowClose(); super.onWindowClose();
} }
@override
Future<void> 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<void> 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 @override
void onWindowMinimize() async { void onWindowMinimize() async {
await globalState.appController.savePreferences(); await globalState.appController.savePreferences();

View File

@@ -1,7 +1,7 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.39+202407151 version: 0.8.41+202407171
environment: environment:
sdk: '>=3.1.0 <4.0.0' sdk: '>=3.1.0 <4.0.0'