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:
319
lib/views/dashboard/dashboard.dart
Normal file
319
lib/views/dashboard/dashboard.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
66
lib/views/dashboard/widgets/intranet_ip.dart
Normal file
66
lib/views/dashboard/widgets/intranet_ip.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
171
lib/views/dashboard/widgets/memory_info.dart
Normal file
171
lib/views/dashboard/widgets/memory_info.dart
Normal 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,
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
158
lib/views/dashboard/widgets/network_detection.dart
Normal file
158
lib/views/dashboard/widgets/network_detection.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
lib/views/dashboard/widgets/network_speed.dart
Normal file
95
lib/views/dashboard/widgets/network_speed.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
162
lib/views/dashboard/widgets/outbound_mode.dart
Normal file
162
lib/views/dashboard/widgets/outbound_mode.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
254
lib/views/dashboard/widgets/quick_options.dart
Normal file
254
lib/views/dashboard/widgets/quick_options.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
148
lib/views/dashboard/widgets/start_button.dart
Normal file
148
lib/views/dashboard/widgets/start_button.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
219
lib/views/dashboard/widgets/traffic_usage.dart
Normal file
219
lib/views/dashboard/widgets/traffic_usage.dart
Normal 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,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/views/dashboard/widgets/widgets.dart
Normal file
7
lib/views/dashboard/widgets/widgets.dart
Normal 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';
|
||||
Reference in New Issue
Block a user