491fae10f5
BREAKING CHANGE: DNS and NTP override toggles are removed; picked keys apply to every profile Changelog: Per-key DNS, NTP, sniffer and TUN settings
275 lines
8.2 KiB
Dart
275 lines
8.2 KiB
Dart
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/icons/icons.dart';
|
|
import 'package:fl_clash/providers/providers.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
const _threeDigitHourThreshold = 100 * 60 * 60 * 1000;
|
|
const _widthAnimationDuration = Duration(milliseconds: 200);
|
|
const _buttonHeight = 56.0;
|
|
const _iconMorphDuration = Duration(milliseconds: 450);
|
|
|
|
TextStyle? _runTimeTextStyle(BuildContext context) {
|
|
return context.textTheme.titleMedium?.toSoftBold.copyWith(
|
|
color: context.colorScheme.onPrimaryContainer,
|
|
fontFeatures: const [FontFeature.tabularFigures()],
|
|
);
|
|
}
|
|
|
|
double _computeRunTimeTextWidth(
|
|
BuildContext context, {
|
|
required bool hasThreeDigitHours,
|
|
}) {
|
|
final sample = hasThreeDigitHours ? '999:99:99' : '99:99:99';
|
|
return globalState.measure
|
|
.computeTextSize(Text(sample, style: _runTimeTextStyle(context)))
|
|
.width +
|
|
16;
|
|
}
|
|
|
|
class RunTimeText extends StatelessWidget {
|
|
final int? timeStamp;
|
|
|
|
const RunTimeText({super.key, required this.timeStamp});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Text(
|
|
getTimeText(timeStamp),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.visible,
|
|
style: _runTimeTextStyle(context),
|
|
);
|
|
}
|
|
}
|
|
|
|
class StartButton extends ConsumerStatefulWidget {
|
|
const StartButton({super.key});
|
|
|
|
@override
|
|
ConsumerState<StartButton> createState() => _StartButtonState();
|
|
}
|
|
|
|
class _StartButtonState extends ConsumerState<StartButton>
|
|
with SingleTickerProviderStateMixin {
|
|
AnimationController? _controller;
|
|
late Animation<double> _animation;
|
|
double? _twoDigitTextWidth;
|
|
double? _threeDigitTextWidth;
|
|
double? _suspendedTextWidth;
|
|
int? _displayRunTime;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final isStart = ref.read(isStartProvider);
|
|
_displayRunTime = ref.read(runTimeProvider);
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
value: isStart ? 1 : 0,
|
|
duration: const Duration(milliseconds: 200),
|
|
);
|
|
_animation = CurvedAnimation(
|
|
parent: _controller!,
|
|
curve: Curves.easeOutBack,
|
|
);
|
|
ref.listenManual(runTimeProvider, (_, next) {
|
|
_updateDisplayRunTime(next);
|
|
});
|
|
ref.listenManual(isStartProvider, (prev, next) {
|
|
updateController(next);
|
|
}, fireImmediately: true);
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
_twoDigitTextWidth = null;
|
|
_threeDigitTextWidth = null;
|
|
_suspendedTextWidth = null;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller?.dispose();
|
|
_controller = null;
|
|
super.dispose();
|
|
}
|
|
|
|
void handleSwitchStart() {
|
|
ref.read(commonActionProvider.notifier).toggleRunning();
|
|
}
|
|
|
|
void _updateDisplayRunTime(int? runTime) {
|
|
if (!mounted ||
|
|
_displayRunTime == runTime ||
|
|
(runTime == null && !(_controller?.isDismissed ?? true))) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_displayRunTime = runTime;
|
|
});
|
|
}
|
|
|
|
void updateController(bool isStart) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
final controller = _controller;
|
|
if (controller == null) {
|
|
return;
|
|
}
|
|
if (isStart) {
|
|
controller.forward();
|
|
return;
|
|
}
|
|
controller.reverse().whenCompleteOrCancel(() {
|
|
if (mounted && controller.isDismissed) {
|
|
_updateDisplayRunTime(ref.read(runTimeProvider));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
double _getRunTimeTextWidth(
|
|
BuildContext context, {
|
|
required bool hasThreeDigitHours,
|
|
}) {
|
|
if (hasThreeDigitHours) {
|
|
return _threeDigitTextWidth ??= _computeRunTimeTextWidth(
|
|
context,
|
|
hasThreeDigitHours: true,
|
|
);
|
|
}
|
|
return _twoDigitTextWidth ??= _computeRunTimeTextWidth(
|
|
context,
|
|
hasThreeDigitHours: false,
|
|
);
|
|
}
|
|
|
|
double _getSuspendedTextWidth(BuildContext context, String suspendedText) {
|
|
return _suspendedTextWidth ??=
|
|
globalState.measure
|
|
.computeTextSize(
|
|
Text(suspendedText, style: context.textTheme.titleMedium),
|
|
)
|
|
.width +
|
|
24;
|
|
}
|
|
|
|
Widget _buildIcon(bool isStart, {required bool suspended}) {
|
|
return AnimatedSwitcher(
|
|
duration: context.motionDuration(commonDuration),
|
|
transitionBuilder: (child, animation) => FadeTransition(
|
|
opacity: animation,
|
|
child: ScaleTransition(scale: animation, child: child),
|
|
),
|
|
child: suspended
|
|
? const GlyphIcon(AppGlyphs.wifiOff, fill: 1)
|
|
: TweenAnimationBuilder<double>(
|
|
tween: Tween(end: isStart ? 1 : 0),
|
|
duration: _iconMorphDuration,
|
|
curve: Curves.easeOutBack,
|
|
builder: (_, progress, _) =>
|
|
GlyphIcon(AppGlyphs.playPause(progress), fill: 1),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final hasProfile = ref.watch(
|
|
profilesProvider.select((state) => state.isNotEmpty),
|
|
);
|
|
if (!hasProfile) {
|
|
return Container();
|
|
}
|
|
final isStart = ref.watch(isStartProvider);
|
|
final suspend = ref.watch(suspendProvider);
|
|
final appLocalizations = context.appLocalizations;
|
|
final suspended = isStart && suspend;
|
|
if (NavigationDock.isDocked(context)) {
|
|
return FloatingActionButton(
|
|
heroTag: null,
|
|
tooltip: suspended ? appLocalizations.suspended : null,
|
|
onPressed: handleSwitchStart,
|
|
child: _buildIcon(isStart, suspended: suspended),
|
|
);
|
|
}
|
|
final hasThreeDigitHours =
|
|
(_displayRunTime ?? 0) >= _threeDigitHourThreshold;
|
|
final theme = Theme.of(context);
|
|
final textWidth = suspend
|
|
? _getSuspendedTextWidth(context, appLocalizations.suspended)
|
|
: _getRunTimeTextWidth(context, hasThreeDigitHours: hasThreeDigitHours);
|
|
return RepaintBoundary(
|
|
child: Theme(
|
|
data: theme.copyWith(
|
|
floatingActionButtonTheme: theme.floatingActionButtonTheme.copyWith(
|
|
sizeConstraints: const BoxConstraints(
|
|
minWidth: 56,
|
|
maxWidth: 220,
|
|
minHeight: _buttonHeight,
|
|
maxHeight: _buttonHeight,
|
|
),
|
|
),
|
|
),
|
|
child: ElasticButton(
|
|
child: FloatingActionButton(
|
|
clipBehavior: Clip.antiAlias,
|
|
materialTapTargetSize: MaterialTapTargetSize.padded,
|
|
heroTag: null,
|
|
onPressed: () {
|
|
handleSwitchStart();
|
|
},
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (_, child) {
|
|
return Container(
|
|
height: _buttonHeight,
|
|
padding: EdgeInsets.only(
|
|
left: 16,
|
|
right: 16 - 8 * _animation.value,
|
|
),
|
|
alignment: Alignment.centerLeft,
|
|
child: child,
|
|
);
|
|
},
|
|
child: _buildIcon(isStart, suspended: suspended),
|
|
),
|
|
SizeTransition(
|
|
axis: Axis.horizontal,
|
|
alignment: Alignment.centerLeft,
|
|
sizeFactor: _animation,
|
|
child: AnimatedContainer(
|
|
width: textWidth,
|
|
duration: _widthAnimationDuration,
|
|
curve: Curves.easeOut,
|
|
child: suspend
|
|
? Text(
|
|
appLocalizations.suspended,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.visible,
|
|
style: Theme.of(context).textTheme.titleMedium
|
|
?.copyWith(
|
|
color: context.colorScheme.onPrimaryContainer,
|
|
),
|
|
)
|
|
: RunTimeText(timeStamp: _displayRunTime),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|