Support override script

Support proxies search

Support svg display

Optimize config persistence

Add some scenes auto close connections

Update core

Optimize more details
This commit is contained in:
chen08209
2025-05-02 02:24:12 +08:00
parent 76c9f08d4a
commit afbc5adb05
174 changed files with 8940 additions and 5433 deletions

View File

@@ -0,0 +1,319 @@
import 'dart:math';
import 'package:defer_pointer/defer_pointer.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'widgets/start_button.dart';
typedef _IsEditWidgetBuilder = Widget Function(bool isEdit);
class DashboardView extends ConsumerStatefulWidget {
const DashboardView({super.key});
@override
ConsumerState<DashboardView> createState() => _DashboardViewState();
}
class _DashboardViewState extends ConsumerState<DashboardView> with PageMixin {
final key = GlobalKey<SuperGridState>();
final _isEditNotifier = ValueNotifier<bool>(false);
final _addedWidgetsNotifier = ValueNotifier<List<GridItem>>([]);
@override
initState() {
ref.listenManual(
isCurrentPageProvider(PageLabel.dashboard),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
return super.initState();
}
@override
dispose() {
_isEditNotifier.dispose();
super.dispose();
}
@override
Widget? get floatingActionButton => const StartButton();
Widget _buildIsEdit(_IsEditWidgetBuilder builder) {
return ValueListenableBuilder(
valueListenable: _isEditNotifier,
builder: (_, isEdit, ___) {
return builder(isEdit);
},
);
}
@override
List<Widget> get actions => [
_buildIsEdit((isEdit) {
return isEdit
? ValueListenableBuilder(
valueListenable: _addedWidgetsNotifier,
builder: (_, addedChildren, child) {
if (addedChildren.isEmpty) {
return Container();
}
return child!;
},
child: IconButton(
onPressed: () {
_showAddWidgetsModal();
},
icon: Icon(
Icons.add_circle,
),
),
)
: SizedBox();
}),
IconButton(
icon: _buildIsEdit((isEdit) {
return isEdit
? Icon(Icons.save)
: Icon(
Icons.edit,
);
}),
onPressed: _handleUpdateIsEdit,
),
];
_showAddWidgetsModal() {
showSheet(
builder: (_, type) {
return ValueListenableBuilder(
valueListenable: _addedWidgetsNotifier,
builder: (_, value, __) {
return AdaptiveSheetScaffold(
type: type,
body: _AddDashboardWidgetModal(
items: value,
onAdd: (gridItem) {
key.currentState?.handleAdd(gridItem);
},
),
title: appLocalizations.add,
);
},
);
},
context: context,
);
}
_handleUpdateIsEdit() {
if (_isEditNotifier.value == true) {
_handleSave();
}
_isEditNotifier.value = !_isEditNotifier.value;
}
_handleSave() {
final children = key.currentState?.children;
if (children == null) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final dashboardWidgets = children
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
);
});
}
@override
Widget build(BuildContext context) {
final dashboardState = ref.watch(dashboardStateProvider);
final columns = max(4 * ((dashboardState.viewWidth / 320).ceil()), 8);
final spacing = 16.ap;
final children = [
...dashboardState.dashboardWidgets
.where(
(item) => item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map(
(item) => item.widget,
),
];
WidgetsBinding.instance.addPostFrameCallback((_) {
_addedWidgetsNotifier.value = DashboardWidget.values
.where(
(item) =>
!children.contains(item.widget) &&
item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map((item) => item.widget)
.toList();
});
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: _buildIsEdit((isEdit) {
return isEdit
? SystemBackBlock(
child: CommonPopScope(
child: SuperGrid(
key: key,
crossAxisCount: columns,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
children: [
...dashboardState.dashboardWidgets
.where(
(item) => item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map(
(item) => item.widget,
),
],
onUpdate: () {
_handleSave();
},
),
onPop: () {
_handleUpdateIsEdit();
return false;
},
),
)
: Grid(
crossAxisCount: columns,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
children: children,
);
})),
);
}
}
class _AddDashboardWidgetModal extends StatelessWidget {
final List<GridItem> items;
final Function(GridItem item) onAdd;
const _AddDashboardWidgetModal({
required this.items,
required this.onAdd,
});
@override
Widget build(BuildContext context) {
return DeferredPointerHandler(
child: SingleChildScrollView(
padding: EdgeInsets.all(
16,
),
child: Grid(
crossAxisCount: 8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: items
.map(
(item) => item.wrap(
builder: (child) {
return _AddedContainer(
onAdd: () {
onAdd(item);
},
child: child,
);
},
),
)
.toList(),
),
),
);
}
}
class _AddedContainer extends StatefulWidget {
final Widget child;
final VoidCallback onAdd;
const _AddedContainer({
required this.child,
required this.onAdd,
});
@override
State<_AddedContainer> createState() => _AddedContainerState();
}
class _AddedContainerState extends State<_AddedContainer> {
@override
void initState() {
super.initState();
}
@override
void didUpdateWidget(_AddedContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.child != widget.child) {}
}
_handleAdd() async {
widget.onAdd();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
ActivateBox(
child: widget.child,
),
Positioned(
top: -8,
right: -8,
child: DeferPointer(
child: SizedBox(
width: 24,
height: 24,
child: IconButton.filled(
iconSize: 20,
padding: EdgeInsets.all(2),
onPressed: _handleAdd,
icon: Icon(
Icons.add,
),
),
),
),
)
],
);
}
}

View File

@@ -0,0 +1,66 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class IntranetIP extends StatelessWidget {
const IntranetIP({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: CommonCard(
info: Info(
label: appLocalizations.intranetIP,
iconData: Icons.devices,
),
onPressed: () {},
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 0,
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: globalState.measure.bodyMediumHeight + 2,
child: Consumer(
builder: (_, ref, __) {
final localIp = ref.watch(localIpProvider);
return FadeThroughBox(
child: localIp != null
? TooltipText(
text: Text(
localIp.isNotEmpty
? localIp
: appLocalizations.noNetwork,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
: Container(
padding: EdgeInsets.all(2),
child: AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
},
),
)
],
),
),
),
);
}
}

View File

@@ -0,0 +1,171 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
final _memoryInfoStateNotifier = ValueNotifier<TrafficValue>(
TrafficValue(value: 0),
);
class MemoryInfo extends StatefulWidget {
const MemoryInfo({super.key});
@override
State<MemoryInfo> createState() => _MemoryInfoState();
}
class _MemoryInfoState extends State<MemoryInfo> {
Timer? timer;
@override
void initState() {
super.initState();
_updateMemory();
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
_updateMemory() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final rss = ProcessInfo.currentRss;
_memoryInfoStateNotifier.value = TrafficValue(
value: clashLib != null ? rss : await clashCore.getMemory() + rss,
);
timer = Timer(Duration(seconds: 2), () async {
_updateMemory();
});
});
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: CommonCard(
info: Info(
iconData: Icons.memory,
label: appLocalizations.memoryInfo,
),
onPressed: () {
clashCore.requestGc();
},
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 0,
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: globalState.measure.bodyMediumHeight + 2,
child: ValueListenableBuilder(
valueListenable: _memoryInfoStateNotifier,
builder: (_, trafficValue, __) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
trafficValue.showValue,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
),
SizedBox(
width: 8,
),
Text(
trafficValue.showUnit,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
)
],
);
},
),
)
],
),
),
),
);
}
}
// class AnimatedCounter extends StatefulWidget {
// final double value;
// final TextStyle? style;
//
// const AnimatedCounter({
// super.key,
// required this.value,
// this.style,
// });
//
// @override
// State<AnimatedCounter> createState() => _AnimatedCounterState();
// }
//
// class _AnimatedCounterState extends State<AnimatedCounter> {
// late double _previousValue;
// late double _currentValue;
//
// @override
// void initState() {
// super.initState();
// _previousValue = widget.value;
// _currentValue = widget.value;
// }
//
// @override
// void didUpdateWidget(AnimatedCounter oldWidget) {
// super.didUpdateWidget(oldWidget);
// if (oldWidget.value != widget.value) {
// // if (_previousValue == _currentValue) {
// // _previousValue = widget.value;
// // _currentValue = widget.value;
// // return;
// // }
// _currentValue = widget.value;
// }
// }
//
// @override
// void dispose() {
// super.dispose();
// }
//
// @override
// Widget build(BuildContext context) {
// return Text(
// _currentValue.fixed(decimals: 1),
// style: widget.style,
// );
// return TweenAnimationBuilder(
// tween: Tween(
// begin: _previousValue,
// end: _currentValue,
// ),
// onEnd: () {
// _previousValue = _currentValue;
// },
// duration: Duration(seconds: 6),
// curve: Curves.easeOut,
// builder: (_, value, ___) {
// return Text(
// value.fixed(decimals: 1),
// style: widget.style,
// );
// },
// );
// }
// }

View File

@@ -0,0 +1,158 @@
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:flutter_riverpod/flutter_riverpod.dart';
class NetworkDetection extends ConsumerStatefulWidget {
const NetworkDetection({super.key});
@override
ConsumerState<NetworkDetection> createState() => _NetworkDetectionState();
}
class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
_countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase();
if (code.length != 2) {
return countryCode;
}
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: ValueListenableBuilder<NetworkDetectionState>(
valueListenable: detectionState.state,
builder: (_, state, __) {
final ipInfo = state.ipInfo;
final isLoading = state.isLoading;
return CommonCard(
onPressed: () {},
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: globalState.measure.titleMediumHeight + 16,
padding: baseInfoEdgeInsets.copyWith(
bottom: 0,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
ipInfo != null
? Text(
_countryCodeToEmoji(
ipInfo.countryCode,
),
style: Theme.of(context)
.textTheme
.titleMedium
?.toLight
.copyWith(
fontFamily: FontFamily.twEmoji.value,
),
)
: Icon(
Icons.network_check,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.networkDetection,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
),
),
SizedBox(width: 2),
AspectRatio(
aspectRatio: 1,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
text: appLocalizations.detectionTip,
),
cancelable: false,
);
},
icon: Icon(
size: 16.ap,
Icons.info_outline,
color: context.colorScheme.onSurfaceVariant,
),
),
)
],
),
),
Container(
padding: baseInfoEdgeInsets.copyWith(
top: 0,
),
child: SizedBox(
height: globalState.measure.bodyMediumHeight + 2,
child: FadeThroughBox(
child: ipInfo != null
? TooltipText(
text: Text(
ipInfo.ip,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
: FadeThroughBox(
child: isLoading == false && ipInfo == null
? Text(
"timeout",
style: context.textTheme.bodyMedium
?.copyWith(color: Colors.red)
.adjustSize(1),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Container(
padding: const EdgeInsets.all(2),
child: const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
),
),
),
)
],
),
);
},
),
);
}
}

View File

@@ -0,0 +1,95 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class NetworkSpeed extends StatefulWidget {
const NetworkSpeed({super.key});
@override
State<NetworkSpeed> createState() => _NetworkSpeedState();
}
class _NetworkSpeedState extends State<NetworkSpeed> {
List<Point> initPoints = const [Point(0, 0), Point(1, 0)];
List<Point> _getPoints(List<Traffic> traffics) {
List<Point> trafficPoints = traffics
.toList()
.asMap()
.map(
(index, e) => MapEntry(
index,
Point(
(index + initPoints.length).toDouble(),
e.speed.toDouble(),
),
),
)
.values
.toList();
return [...initPoints, ...trafficPoints];
}
Traffic _getLastTraffic(List<Traffic> traffics) {
if (traffics.isEmpty) return Traffic();
return traffics.last;
}
@override
Widget build(BuildContext context) {
final color = context.colorScheme.onSurfaceVariant.opacity80;
return SizedBox(
height: getWidgetHeight(2),
child: CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.networkSpeed,
iconData: Icons.speed_sharp,
),
child: Consumer(
builder: (_, ref, __) {
final traffics = ref.watch(trafficsProvider).list;
return Stack(
children: [
Positioned.fill(
child: Padding(
padding: EdgeInsets.all(16).copyWith(
bottom: 0,
left: 0,
right: 0,
),
child: LineChart(
gradient: true,
color: Theme.of(context).colorScheme.primary,
points: _getPoints(traffics),
),
),
),
Positioned(
top: 0,
right: 0,
child: Transform.translate(
offset: Offset(
-16,
-20,
),
child: Text(
"${_getLastTraffic(traffics).up}${_getLastTraffic(traffics).down}",
style: context.textTheme.bodySmall?.copyWith(
color: color,
),
),
),
),
],
);
},
),
),
);
}
}

View File

@@ -0,0 +1,162 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
class OutboundMode extends StatelessWidget {
const OutboundMode({super.key});
@override
Widget build(BuildContext context) {
final height = getWidgetHeight(2);
return SizedBox(
height: height,
child: Consumer(
builder: (_, ref, __) {
final mode = ref.watch(
patchClashConfigProvider.select(
(state) => state.mode,
),
);
return Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent),
child: CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.outboundMode,
iconData: Icons.call_split_sharp,
),
child: Padding(
padding: const EdgeInsets.only(
top: 12,
bottom: 16,
),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (final item in Mode.values)
Flexible(
fit: FlexFit.tight,
child: ListItem.radio(
dense: true,
horizontalTitleGap: 4,
padding: EdgeInsets.only(
left: 12.ap,
right: 16.ap,
),
delegate: RadioDelegate(
value: item,
groupValue: mode,
onChanged: (value) async {
if (value == null) {
return;
}
globalState.appController.changeMode(value);
},
),
title: Text(
Intl.message(item.name),
style: Theme.of(context)
.textTheme
.bodyMedium
?.toSoftBold,
),
),
),
],
),
),
));
},
),
);
}
}
class OutboundModeV2 extends StatelessWidget {
const OutboundModeV2({super.key});
Color _getTextColor(BuildContext context, Mode mode) {
return switch (mode) {
Mode.rule => context.colorScheme.onSecondaryContainer,
Mode.global => context.colorScheme.onPrimaryContainer,
Mode.direct => context.colorScheme.onTertiaryContainer,
};
}
@override
Widget build(BuildContext context) {
final height = getWidgetHeight(0.72);
return SizedBox(
height: height,
child: CommonCard(
padding: EdgeInsets.zero,
child: Consumer(
builder: (_, ref, __) {
final mode = ref.watch(
patchClashConfigProvider.select(
(state) => state.mode,
),
);
final thumbColor = switch (mode) {
Mode.rule => context.colorScheme.secondaryContainer,
Mode.global => globalState.theme.darken3PrimaryContainer,
Mode.direct => context.colorScheme.tertiaryContainer,
};
return Container(
constraints: BoxConstraints.expand(),
child: CommonTabBar<Mode>(
children: Map.fromEntries(
Mode.values.map(
(item) => MapEntry(
item,
Container(
clipBehavior: Clip.antiAlias,
alignment: Alignment.center,
decoration: BoxDecoration(),
height: height - 16,
child: Text(
Intl.message(item.name),
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(1)
.copyWith(
color: item == mode
? _getTextColor(
context,
item,
)
: null,
),
),
),
),
),
),
padding: EdgeInsets.symmetric(horizontal: 8),
groupValue: mode,
onValueChanged: (value) {
if (value == null) {
return;
}
globalState.appController.changeMode(value);
},
thumbColor: thumbColor,
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,254 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/views/config/network.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TUNButton extends StatelessWidget {
const TUNButton({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: CommonCard(
onPressed: () {
showSheet(
context: context,
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: generateListView(
generateSection(
items: [
if (system.isDesktop) const TUNItem(),
if (Platform.isMacOS) const AutoSetSystemDnsItem(),
const TunStackItem(),
],
),
),
title: appLocalizations.tun,
);
},
);
},
info: Info(
label: appLocalizations.tun,
iconData: Icons.stacked_line_chart,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
),
),
Consumer(
builder: (_, ref, __) {
final enable = ref.watch(patchClashConfigProvider
.select((state) => state.tun.enable));
return Switch(
value: enable,
onChanged: (value) {
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith.tun(
enable: value,
),
);
},
);
},
)
],
),
),
),
);
}
}
class SystemProxyButton extends StatelessWidget {
const SystemProxyButton({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: CommonCard(
onPressed: () {
showSheet(
context: context,
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: generateListView(
generateSection(
items: [
SystemProxyItem(),
BypassDomainItem(),
],
),
),
title: appLocalizations.systemProxy,
);
},
);
},
info: Info(
label: appLocalizations.systemProxy,
iconData: Icons.shuffle,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
),
),
Consumer(
builder: (_, ref, __) {
final systemProxy = ref.watch(networkSettingProvider
.select((state) => state.systemProxy));
return Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: systemProxy,
onChanged: (value) {
ref.read(networkSettingProvider.notifier).updateState(
(state) => state.copyWith(
systemProxy: value,
),
);
},
);
},
)
],
),
),
),
);
}
}
class VpnButton extends StatelessWidget {
const VpnButton({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: CommonCard(
onPressed: () {
showSheet(
context: context,
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: generateListView(
generateSection(
items: [
const VPNItem(),
const VpnSystemProxyItem(),
const TunStackItem(),
],
),
),
title: "VPN",
);
},
);
},
info: Info(
label: "VPN",
iconData: Icons.stacked_line_chart,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
),
),
Consumer(
builder: (_, ref, __) {
final enable = ref.watch(
vpnSettingProvider.select(
(state) => state.enable,
),
);
return Switch(
value: enable,
onChanged: (value) {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith(
enable: value,
),
);
},
);
},
)
],
),
),
),
);
}
}

View File

@@ -0,0 +1,148 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class StartButton extends ConsumerStatefulWidget {
const StartButton({super.key});
@override
ConsumerState<StartButton> createState() => _StartButtonState();
}
class _StartButtonState extends ConsumerState<StartButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
bool isStart = false;
@override
void initState() {
super.initState();
isStart = globalState.appState.runTime != null;
_controller = AnimationController(
vsync: this,
value: isStart ? 1 : 0,
duration: const Duration(milliseconds: 200),
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutBack,
);
ref.listenManual(
runTimeProvider.select((state) => state != null),
(prev, next) {
if (next != isStart) {
isStart = next;
updateController();
}
},
fireImmediately: true,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
handleSwitchStart() {
isStart = !isStart;
updateController();
debouncer.call(
FunctionTag.updateStatus,
() {
globalState.appController.updateStatus(isStart);
},
duration: commonDuration,
);
}
updateController() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (isStart) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
@override
Widget build(BuildContext context) {
final state = ref.watch(startButtonSelectorStateProvider);
if (!state.isInit || !state.hasProfile) {
return Container();
}
return Theme(
data: Theme.of(context).copyWith(
floatingActionButtonTheme: FloatingActionButtonThemeData(
sizeConstraints: BoxConstraints(
minWidth: 56,
maxWidth: 200,
),
),
),
child: AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
final textWidth = globalState.measure
.computeTextSize(
Text(
utils.getTimeDifference(
DateTime.now(),
),
style: context.textTheme.titleMedium?.toSoftBold,
),
)
.width +
16;
return FloatingActionButton(
clipBehavior: Clip.antiAlias,
materialTapTargetSize: MaterialTapTargetSize.padded,
heroTag: null,
onPressed: () {
handleSwitchStart();
},
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Container(
height: 56,
width: 56,
alignment: Alignment.center,
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _animation,
),
),
SizedBox(
width: textWidth * _animation.value,
child: child!,
)
],
),
);
},
child: Consumer(
builder: (_, ref, __) {
final runTime = ref.watch(runTimeProvider);
final text = utils.getTimeText(runTime);
return Text(
text,
maxLines: 1,
overflow: TextOverflow.visible,
style:
Theme.of(context).textTheme.titleMedium?.toSoftBold.copyWith(
color: context.colorScheme.onPrimaryContainer,
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,219 @@
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TrafficUsage extends StatelessWidget {
const TrafficUsage({super.key});
Widget _buildTrafficDataItem(
BuildContext context,
Icon icon,
TrafficValue trafficValue,
) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
icon,
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: Text(
trafficValue.showValue,
style: context.textTheme.bodySmall,
maxLines: 1,
),
),
],
),
),
Text(
trafficValue.showUnit,
style: context.textTheme.bodySmall?.toLighter,
),
],
);
}
@override
Widget build(BuildContext context) {
final primaryColor = globalState.theme.darken3PrimaryContainer;
final secondaryColor = globalState.theme.darken2SecondaryContainer;
return SizedBox(
height: getWidgetHeight(2),
child: CommonCard(
info: Info(
label: appLocalizations.trafficUsage,
iconData: Icons.data_saver_off,
),
onPressed: () {},
child: Consumer(
builder: (_, ref, __) {
final totalTraffic = ref.watch(totalTrafficProvider);
final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down;
return Padding(
padding: baseInfoEdgeInsets.copyWith(
top: 0,
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Container(
padding: EdgeInsets.symmetric(
vertical: 12,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AspectRatio(
aspectRatio: 1,
child: DonutChart(
data: [
DonutChartData(
value: upTotalTrafficValue.value.toDouble(),
color: primaryColor,
),
DonutChartData(
value: downTotalTrafficValue.value.toDouble(),
color: secondaryColor,
),
],
),
),
SizedBox(
width: 8,
),
Flexible(
child: LayoutBuilder(
builder: (_, container) {
final uploadText = Text(
maxLines: 1,
appLocalizations.upload,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall,
);
final downloadText = Text(
maxLines: 1,
appLocalizations.download,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall,
);
final uploadTextSize = globalState.measure
.computeTextSize(uploadText);
final downloadTextSize = globalState.measure
.computeTextSize(downloadText);
final maxTextWidth = max(uploadTextSize.width,
downloadTextSize.width);
if (maxTextWidth + 24 > container.maxWidth) {
return Container();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 20,
height: 8,
decoration: BoxDecoration(
color: primaryColor,
borderRadius:
BorderRadius.circular(2),
),
),
SizedBox(
width: 4,
),
Text(
maxLines: 1,
appLocalizations.upload,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall,
),
],
),
SizedBox(
height: 4,
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 20,
height: 8,
decoration: BoxDecoration(
color: secondaryColor,
borderRadius:
BorderRadius.circular(2),
),
),
SizedBox(
width: 4,
),
Text(
maxLines: 1,
appLocalizations.download,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall,
),
],
),
],
);
},
),
),
],
),
),
),
_buildTrafficDataItem(
context,
Icon(
Icons.arrow_upward,
color: primaryColor,
size: 14,
),
upTotalTrafficValue,
),
const SizedBox(
height: 8,
),
_buildTrafficDataItem(
context,
Icon(
Icons.arrow_downward,
color: secondaryColor,
size: 14,
),
downTotalTrafficValue,
)
],
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,7 @@
export 'intranet_ip.dart';
export 'network_detection.dart';
export 'network_speed.dart';
export 'outbound_mode.dart';
export 'quick_options.dart';
export 'traffic_usage.dart';
export 'memory_info.dart';