Optimize focus control
This commit is contained in:
+104
-87
@@ -6,6 +6,7 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/manager/window_manager.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/animated_visibility.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -87,9 +88,6 @@ class _AppStateManagerState extends ConsumerState<AppStateManager>
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final ref = globalState.container;
|
||||
ref.read(setupActionProvider.notifier).tryCheckIp();
|
||||
if (system.isAndroid) {
|
||||
ref.read(coreActionProvider.notifier).tryStartCore();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -128,7 +126,7 @@ class AppEnvManager extends StatelessWidget {
|
||||
}
|
||||
if (globalState.isPre) {
|
||||
return Banner(
|
||||
message: 'PRE',
|
||||
message: globalState.appEnv.toUpperCase(),
|
||||
location: BannerLocation.topEnd,
|
||||
child: child,
|
||||
);
|
||||
@@ -183,9 +181,21 @@ class AppSidebarContainer extends ConsumerWidget {
|
||||
}
|
||||
|
||||
void _handleToPage(PageLabel pageLabel) {
|
||||
final focusNode = FocusManager.instance.primaryFocus;
|
||||
final preserveNavigationFocus =
|
||||
focusNode?.context?.findAncestorWidgetOfExactType<NavigationRail>() !=
|
||||
null;
|
||||
globalState.container
|
||||
.read(currentPageLabelProvider.notifier)
|
||||
.toPage(pageLabel);
|
||||
if (!preserveNavigationFocus || focusNode == null) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (focusNode.context != null && focusNode.canRequestFocus) {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -193,98 +203,105 @@ class AppSidebarContainer extends ConsumerWidget {
|
||||
final navigationState = ref.watch(navigationStateProvider);
|
||||
final navigationItems = navigationState.navigationItems;
|
||||
final isMobileView = navigationState.viewMode == ViewMode.mobile;
|
||||
if (isMobileView) {
|
||||
return child;
|
||||
}
|
||||
final currentIndex = navigationState.currentIndex;
|
||||
final showLabel = ref.watch(appSettingProvider).showLabel;
|
||||
return Row(
|
||||
children: [
|
||||
_buildBackground(
|
||||
context: context,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (system.isMacOS) const SizedBox(height: 22),
|
||||
const SizedBox(height: 10),
|
||||
if (!system.isMacOS) ...[
|
||||
const ClipRect(child: AppIcon()),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: ScrollConfiguration(
|
||||
behavior: HiddenBarScrollBehavior(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: NavigationRail(
|
||||
scrollable: true,
|
||||
minExtendedWidth: 200,
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedLabelTextStyle: context
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(color: context.colorScheme.onSurface),
|
||||
unselectedLabelTextStyle: context
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(color: context.colorScheme.onSurface),
|
||||
destinations: navigationItems
|
||||
.map(
|
||||
(e) => NavigationRailDestination(
|
||||
icon: e.icon,
|
||||
label: Text(Intl.message(e.label.name)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onDestinationSelected: (index) {
|
||||
_handleToPage(navigationItems[index].label);
|
||||
},
|
||||
extended: false,
|
||||
selectedIndex: currentIndex,
|
||||
labelType: showLabel
|
||||
? NavigationRailLabelType.all
|
||||
: NavigationRailLabelType.none,
|
||||
),
|
||||
return Container(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedVisibility.sidebar(
|
||||
visible: !isMobileView,
|
||||
child: _buildBackground(
|
||||
context: context,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (system.isMacOS) const SizedBox(height: 22),
|
||||
const SizedBox(height: 10),
|
||||
if (!system.isMacOS) ...[
|
||||
const ClipRect(child: AppIcon()),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: ScrollConfiguration(
|
||||
behavior: HiddenBarScrollBehavior(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: NavigationRail(
|
||||
scrollable: true,
|
||||
minExtendedWidth: 200,
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedLabelTextStyle: context
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
),
|
||||
unselectedLabelTextStyle: context
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
),
|
||||
destinations: navigationItems
|
||||
.map(
|
||||
(e) => NavigationRailDestination(
|
||||
icon: e.icon,
|
||||
label: Text(Intl.message(e.label.name)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onDestinationSelected: (index) {
|
||||
_handleToPage(navigationItems[index].label);
|
||||
},
|
||||
extended: false,
|
||||
selectedIndex: currentIndex,
|
||||
labelType: showLabel
|
||||
? NavigationRailLabelType.all
|
||||
: NavigationRailLabelType.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(appSettingProvider.notifier)
|
||||
.update(
|
||||
(state) =>
|
||||
state.copyWith(showLabel: !state.showLabel),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.menu,
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(appSettingProvider.notifier)
|
||||
.update(
|
||||
(state) =>
|
||||
state.copyWith(showLabel: !state.showLabel),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.menu,
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ClipRect(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
_updateSideBarWidth(ref, constraints.maxWidth);
|
||||
return child;
|
||||
},
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ClipRect(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
_updateSideBarWidth(ref, constraints.maxWidth);
|
||||
return child;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,67 @@
|
||||
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';
|
||||
|
||||
const _threeDigitHourThreshold = 100 * 60 * 60 * 1000;
|
||||
const _widthAnimationDuration = Duration(milliseconds: 200);
|
||||
const _buttonHeight = 56.0;
|
||||
|
||||
TextStyle? _runTimeTextStyle(BuildContext context) {
|
||||
return context.textTheme.titleMedium?.toSoftBold.copyWith(
|
||||
color: context.colorScheme.onPrimaryContainer,
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? _hundredsTextStyle(BuildContext context) {
|
||||
return context.textTheme.titleMedium?.toSoftBold.copyWith(
|
||||
color: context.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
}
|
||||
|
||||
double _computeRunTimeTextWidth(
|
||||
BuildContext context, {
|
||||
required bool hasThreeDigitHours,
|
||||
}) {
|
||||
final regularWidth = globalState.measure
|
||||
.computeTextSize(Text('99:99:99', style: _runTimeTextStyle(context)))
|
||||
.width;
|
||||
if (!hasThreeDigitHours) {
|
||||
return regularWidth + 16;
|
||||
}
|
||||
final hundredsWidth = globalState.measure
|
||||
.computeTextSize(Text('9', style: _hundredsTextStyle(context)))
|
||||
.width;
|
||||
return hundredsWidth + regularWidth + 16;
|
||||
}
|
||||
|
||||
class RunTimeText extends StatelessWidget {
|
||||
final int? timeStamp;
|
||||
|
||||
const RunTimeText({super.key, required this.timeStamp});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = utils.getTimeText(timeStamp);
|
||||
final style = _runTimeTextStyle(context);
|
||||
final textWidget = text.length < 9
|
||||
? Text(text, maxLines: 1, overflow: TextOverflow.visible, style: style)
|
||||
: Text.rich(
|
||||
TextSpan(
|
||||
text: text.substring(0, 1),
|
||||
style: _hundredsTextStyle(context),
|
||||
children: [TextSpan(text: text.substring(1), style: style)],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.visible,
|
||||
style: style,
|
||||
);
|
||||
return textWidget;
|
||||
}
|
||||
}
|
||||
|
||||
class StartButton extends ConsumerStatefulWidget {
|
||||
const StartButton({super.key});
|
||||
|
||||
@@ -16,12 +73,16 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController? _controller;
|
||||
late Animation<double> _animation;
|
||||
bool isStart = false;
|
||||
double? _twoDigitTextWidth;
|
||||
double? _threeDigitTextWidth;
|
||||
double? _suspendedTextWidth;
|
||||
int? _displayRunTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isStart = ref.read(isStartProvider);
|
||||
final isStart = ref.read(isStartProvider);
|
||||
_displayRunTime = ref.read(runTimeProvider);
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
value: isStart ? 1 : 0,
|
||||
@@ -31,14 +92,22 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
parent: _controller!,
|
||||
curve: Curves.easeOutBack,
|
||||
);
|
||||
ref.listenManual(runTimeProvider, (_, next) {
|
||||
_updateDisplayRunTime(next);
|
||||
});
|
||||
ref.listenManual(isStartProvider, (prev, next) {
|
||||
if (next != isStart) {
|
||||
isStart = next;
|
||||
updateController();
|
||||
}
|
||||
updateController(next);
|
||||
}, fireImmediately: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_twoDigitTextWidth = null;
|
||||
_threeDigitTextWidth = null;
|
||||
_suspendedTextWidth = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
@@ -47,25 +116,67 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
}
|
||||
|
||||
void handleSwitchStart() {
|
||||
isStart = !isStart;
|
||||
updateController();
|
||||
debouncer.call(FunctionTag.updateStatus, () {
|
||||
globalState.container
|
||||
.read(setupActionProvider.notifier)
|
||||
.updateStatus(isStart, isInit: !ref.read(initProvider));
|
||||
}, duration: commonDuration);
|
||||
ref.read(commonActionProvider.notifier).toggleRunning();
|
||||
}
|
||||
|
||||
void updateController() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isStart && mounted) {
|
||||
_controller?.forward();
|
||||
} else {
|
||||
_controller?.reverse();
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasProfile = ref.watch(
|
||||
@@ -75,88 +186,76 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
return Container();
|
||||
}
|
||||
final suspend = ref.watch(suspendProvider);
|
||||
final hasThreeDigitHours =
|
||||
(_displayRunTime ?? 0) >= _threeDigitHourThreshold;
|
||||
final theme = Theme.of(context);
|
||||
final appLocalizations = context.appLocalizations;
|
||||
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: 200),
|
||||
sizeConstraints: const BoxConstraints(
|
||||
minWidth: 56,
|
||||
maxWidth: 220,
|
||||
minHeight: _buttonHeight,
|
||||
maxHeight: _buttonHeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller!.view,
|
||||
builder: (_, child) {
|
||||
final textWidth = suspend
|
||||
? globalState.measure
|
||||
.computeTextSize(
|
||||
Text(
|
||||
appLocalizations.suspended,
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
)
|
||||
.width +
|
||||
24
|
||||
: 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.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 56,
|
||||
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: AnimatedIcon(
|
||||
icon: AnimatedIcons.play_pause,
|
||||
progress: _animation,
|
||||
),
|
||||
),
|
||||
SizedBox(width: textWidth * _animation.value, child: child!),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: suspend
|
||||
? Text(
|
||||
appLocalizations.suspended,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.visible,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: context.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
)
|
||||
: 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: AnimatedIcon(
|
||||
icon: AnimatedIcons.play_pause,
|
||||
progress: _animation,
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -50,15 +50,11 @@ class _EditProfileViewState extends State<EditProfileView> {
|
||||
|
||||
Future<void> _updateFileInfo() async {
|
||||
final file = await widget.profile.file;
|
||||
if (!await file.exists()) {
|
||||
return;
|
||||
}
|
||||
final lastModified = await file.lastModified();
|
||||
final size = await file.length();
|
||||
final fileInfo = await file.getFileInfo();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_fileInfoNotifier.value = FileInfo(size: size, lastModified: lastModified);
|
||||
_fileInfoNotifier.value = fileInfo;
|
||||
}
|
||||
|
||||
Future<void> _handleConfirm() async {
|
||||
@@ -257,12 +253,10 @@ class _EditProfileViewState extends State<EditProfileView> {
|
||||
},
|
||||
),
|
||||
),
|
||||
ListItem.switchItem(
|
||||
ListItem.toggle(
|
||||
title: Text(appLocalizations.autoUpdate),
|
||||
delegate: SwitchDelegate<bool>(
|
||||
value: _autoUpdate,
|
||||
onChanged: _setAutoUpdate,
|
||||
),
|
||||
value: _autoUpdate,
|
||||
onChanged: _setAutoUpdate,
|
||||
),
|
||||
if (_autoUpdate)
|
||||
ListItem(
|
||||
@@ -330,36 +324,40 @@ class _EditProfileViewState extends State<EditProfileView> {
|
||||
},
|
||||
),
|
||||
];
|
||||
return CommonPopScope(
|
||||
onPop: (context) {
|
||||
if (_fileData == null) {
|
||||
return true;
|
||||
}
|
||||
_handleBack();
|
||||
return false;
|
||||
},
|
||||
child: FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: null,
|
||||
onPressed: _handleConfirm,
|
||||
label: Text(appLocalizations.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: ListView.separated(
|
||||
padding: kMaterialListPadding.copyWith(bottom: 72),
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
separatorBuilder: (_, _) {
|
||||
return const SizedBox(height: 24);
|
||||
},
|
||||
itemCount: items.length,
|
||||
return FocusTraversalGroup(
|
||||
policy: PageTraversalPolicy(),
|
||||
child: PageFocusScope(
|
||||
child: CommonPopScope(
|
||||
onPop: (context) {
|
||||
if (_fileData == null) {
|
||||
return true;
|
||||
}
|
||||
_handleBack();
|
||||
return false;
|
||||
},
|
||||
child: FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: CommonFloatingActionButton(
|
||||
onPressed: _handleConfirm,
|
||||
icon: const Icon(Icons.save),
|
||||
label: appLocalizations.save,
|
||||
),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: ListView.separated(
|
||||
padding: kMaterialListPadding.copyWith(bottom: 72),
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
separatorBuilder: (_, _) {
|
||||
return const SizedBox(height: 24);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -132,37 +132,47 @@ class _ProfilesViewState extends State<ProfilesView> {
|
||||
label: appLocalizations.nullProfileDesc,
|
||||
illustration: const ProfileEmptyIllustration(),
|
||||
)
|
||||
: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
key: profilesStoreKey,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 88,
|
||||
),
|
||||
child: Grid(
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
crossAxisCount: state.columns,
|
||||
children: [
|
||||
for (int i = 0; i < state.profiles.length; i++)
|
||||
GridItem(
|
||||
child: ProfileItem(
|
||||
profile: state.profiles[i],
|
||||
groupValue: state.currentProfileId,
|
||||
onChanged: (profileId) {
|
||||
ref
|
||||
.read(currentProfileIdProvider.notifier)
|
||||
.value =
|
||||
profileId;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
: LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
const horizontalPadding = 16.0;
|
||||
final columns = utils.getProfilesColumns(
|
||||
constraints.maxWidth - horizontalPadding * 2,
|
||||
);
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
key: profilesStoreKey,
|
||||
padding: const EdgeInsets.only(
|
||||
left: horizontalPadding,
|
||||
right: horizontalPadding,
|
||||
top: 16,
|
||||
bottom: 88,
|
||||
),
|
||||
child: Grid(
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
crossAxisCount: columns,
|
||||
children: [
|
||||
for (int i = 0; i < state.profiles.length; i++)
|
||||
GridItem(
|
||||
child: ProfileItem(
|
||||
profile: state.profiles[i],
|
||||
groupValue: state.currentProfileId,
|
||||
onChanged: (profileId) {
|
||||
ref
|
||||
.read(
|
||||
currentProfileIdProvider.notifier,
|
||||
)
|
||||
.value =
|
||||
profileId;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -277,6 +287,7 @@ class ProfileItem extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
return CommonCard(
|
||||
enterActionsOnRight: true,
|
||||
isSelected: profile.id == groupValue,
|
||||
onPressed: () {
|
||||
onChanged(profile.id);
|
||||
@@ -298,7 +309,7 @@ class ProfileItem extends StatelessWidget {
|
||||
? const Padding(
|
||||
key: ValueKey('loading'),
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CircularProgressIndicator(),
|
||||
child: CommonCircleLoading(),
|
||||
)
|
||||
: CommonPopupBox(
|
||||
key: const ValueKey('menu'),
|
||||
@@ -341,19 +352,6 @@ class ProfileItem extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
// PopupMenuItemData(
|
||||
// icon: Icons.extension_outlined,
|
||||
// label: appLocalizations.override + "1",
|
||||
// onPressed: () {
|
||||
// final overrideProfileView = OverrideProfileView(
|
||||
// profileId: profile.id,
|
||||
// );
|
||||
// BaseNavigator.push(
|
||||
// context,
|
||||
// overrideProfileView,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
if (profile.type == ProfileType.url) ...[
|
||||
PopupMenuItemData(
|
||||
icon: Icons.copy,
|
||||
|
||||
+131
-203
@@ -23,58 +23,11 @@ class ProxiesListView extends StatefulWidget {
|
||||
|
||||
class _ProxiesListViewState extends State<ProxiesListView> {
|
||||
final _controller = ScrollController();
|
||||
final _headerStateNotifier = ValueNotifier<ProxiesListHeaderSelectorState?>(
|
||||
null,
|
||||
);
|
||||
List<double> _headerOffset = [];
|
||||
List<double> _groupOffsets = [];
|
||||
double containerHeight = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(_adjustHeader);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_adjustHeader();
|
||||
});
|
||||
}
|
||||
|
||||
ProxiesListHeaderSelectorState _getProxiesListHeaderSelectorState(
|
||||
double initOffset,
|
||||
) {
|
||||
final index = _headerOffset.findInterval(initOffset);
|
||||
final currentIndex = index;
|
||||
double headerOffset = 0.0;
|
||||
if (index + 1 <= _headerOffset.length - 1) {
|
||||
final endOffset = _headerOffset[index + 1];
|
||||
final startOffset = endOffset - listHeaderHeight - 8;
|
||||
if (initOffset > startOffset && initOffset < endOffset) {
|
||||
headerOffset = initOffset - startOffset;
|
||||
}
|
||||
}
|
||||
return ProxiesListHeaderSelectorState(
|
||||
offset: max(headerOffset, 0),
|
||||
currentIndex: currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
void _adjustHeader() {
|
||||
_headerStateNotifier.value = _getProxiesListHeaderSelectorState(
|
||||
!_controller.hasClients ? 0 : _controller.offset,
|
||||
);
|
||||
}
|
||||
|
||||
double _getListItemHeight(Type type, ProxyCardType proxyCardType) {
|
||||
return switch (type) {
|
||||
const (SizedBox) => 8,
|
||||
const (ListHeader) => listHeaderHeight,
|
||||
Type() => getItemHeight(proxyCardType),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_headerStateNotifier.dispose();
|
||||
_controller.removeListener(_adjustHeader);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -88,112 +41,117 @@ class _ProxiesListViewState extends State<ProxiesListView> {
|
||||
tempUnfoldSet.add(groupName);
|
||||
}
|
||||
updateCurrentUnfoldSet(tempUnfoldSet);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_adjustHeader();
|
||||
});
|
||||
}
|
||||
|
||||
List<double> _getItemHeightList(
|
||||
List<Widget> items,
|
||||
ProxyCardType proxyCardType,
|
||||
) {
|
||||
final itemHeightList = <double>[];
|
||||
final List<double> headerOffset = [];
|
||||
double currentHeight = 0;
|
||||
for (final item in items) {
|
||||
if (item.runtimeType == ListHeader) {
|
||||
headerOffset.add(currentHeight);
|
||||
}
|
||||
final itemHeight = _getListItemHeight(item.runtimeType, proxyCardType);
|
||||
itemHeightList.add(itemHeight);
|
||||
currentHeight = currentHeight + itemHeight;
|
||||
}
|
||||
_headerOffset = headerOffset;
|
||||
return itemHeightList;
|
||||
}
|
||||
|
||||
List<Widget> _buildItems(
|
||||
WidgetRef ref, {
|
||||
List<double> _getGroupOffsets({
|
||||
required List<Group> groups,
|
||||
required int columns,
|
||||
required Set<String> currentUnfoldSet,
|
||||
required ProxyCardType cardType,
|
||||
}) {
|
||||
final items = <Widget>[];
|
||||
final offsets = <double>[];
|
||||
final rowExtent = getItemHeight(cardType) + 8;
|
||||
var currentOffset = 0.0;
|
||||
for (final group in groups) {
|
||||
final groupName = group.name;
|
||||
final isExpand = currentUnfoldSet.contains(groupName);
|
||||
items.addAll([
|
||||
ListHeader(
|
||||
onScrollToSelected: _scrollToGroupSelected,
|
||||
isExpand: isExpand,
|
||||
group: group,
|
||||
onChange: (String groupName) {
|
||||
_handleChange(currentUnfoldSet, groupName);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
]);
|
||||
if (isExpand) {
|
||||
final proxies = group.all;
|
||||
final chunks = proxies.chunks(columns);
|
||||
final rows = chunks
|
||||
.map<Widget>((proxies) {
|
||||
final children = proxies
|
||||
.map<Widget>(
|
||||
(proxy) => Flexible(
|
||||
child: SizedBox(
|
||||
height: getItemHeight(cardType),
|
||||
child: ProxyCard(
|
||||
testUrl: group.testUrl,
|
||||
type: cardType,
|
||||
groupType: group.type,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.fill(
|
||||
columns,
|
||||
filler: (_) => const Flexible(child: SizedBox()),
|
||||
)
|
||||
.separated(const SizedBox(width: 8));
|
||||
|
||||
return Row(children: children.toList());
|
||||
})
|
||||
.separated(const SizedBox(height: 8));
|
||||
items.addAll([...rows, const SizedBox(height: 8)]);
|
||||
offsets.add(currentOffset);
|
||||
currentOffset += listHeaderHeight + 8;
|
||||
if (currentUnfoldSet.contains(group.name)) {
|
||||
final rowCount = (group.all.length + columns - 1) ~/ columns;
|
||||
currentOffset += rowCount * rowExtent;
|
||||
}
|
||||
}
|
||||
return items;
|
||||
return offsets;
|
||||
}
|
||||
|
||||
Widget _buildHeader(
|
||||
WidgetRef ref, {
|
||||
Widget _buildProxyRow({
|
||||
required Group group,
|
||||
required List<Proxy> proxies,
|
||||
required int columns,
|
||||
required ProxyCardType cardType,
|
||||
}) {
|
||||
final groupName = group.name;
|
||||
final children = proxies
|
||||
.map<Widget>(
|
||||
(proxy) => Flexible(
|
||||
child: SizedBox(
|
||||
height: getItemHeight(cardType),
|
||||
child: ProxyCard(
|
||||
testUrl: group.testUrl,
|
||||
type: cardType,
|
||||
groupType: group.type,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.fill(columns, filler: (_) => const Flexible(child: SizedBox()))
|
||||
.separated(const SizedBox(width: 8));
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
|
||||
child: Row(children: children.toList()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGroup(
|
||||
BuildContext context, {
|
||||
required Group group,
|
||||
required Set<String> currentUnfoldSet,
|
||||
required int columns,
|
||||
required ProxyCardType cardType,
|
||||
}) {
|
||||
final groupName = group.name;
|
||||
final isExpand = currentUnfoldSet.contains(groupName);
|
||||
return SizedBox(
|
||||
height: listHeaderHeight,
|
||||
child: ListHeader(
|
||||
enterAnimated: false,
|
||||
onScrollToSelected: _scrollToGroupSelected,
|
||||
key: Key(groupName),
|
||||
isExpand: isExpand,
|
||||
group: group,
|
||||
onChange: (String groupName) {
|
||||
_handleChange(currentUnfoldSet, groupName);
|
||||
},
|
||||
),
|
||||
final rows = isExpand
|
||||
? group.all.chunks(columns).toList()
|
||||
: const <List<Proxy>>[];
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
PinnedHeaderSliver(
|
||||
child: ColoredBox(
|
||||
color: context.colorScheme.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
|
||||
child: SizedBox(
|
||||
height: listHeaderHeight,
|
||||
child: ListHeader(
|
||||
enterAnimated: false,
|
||||
onScrollToSelected: (groupName) {
|
||||
_scrollToGroupSelected(groupName, columns);
|
||||
},
|
||||
key: ValueKey(groupName),
|
||||
isExpand: isExpand,
|
||||
group: group,
|
||||
onChange: (groupName) {
|
||||
_handleChange(currentUnfoldSet, groupName);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isExpand)
|
||||
SliverFixedExtentList(
|
||||
itemExtent: getItemHeight(cardType) + 8,
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(_, index) => _buildProxyRow(
|
||||
group: group,
|
||||
proxies: rows[index],
|
||||
columns: columns,
|
||||
cardType: cardType,
|
||||
),
|
||||
childCount: rows.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
double _getGroupOffset(String groupName) {
|
||||
if (_controller.position.maxScrollExtent == 0) {
|
||||
if (!_controller.hasClients ||
|
||||
_controller.position.maxScrollExtent == 0 ||
|
||||
_groupOffsets.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
final currentGroups = getCurrentGroups();
|
||||
@@ -201,7 +159,7 @@ class _ProxiesListViewState extends State<ProxiesListView> {
|
||||
(item) => item.name == groupName,
|
||||
);
|
||||
final index = findIndex != -1 ? findIndex : 0;
|
||||
return _headerOffset[index];
|
||||
return _groupOffsets[index];
|
||||
}
|
||||
|
||||
void _scrollToMakeVisibleWithPadding({
|
||||
@@ -254,7 +212,7 @@ class _ProxiesListViewState extends State<ProxiesListView> {
|
||||
);
|
||||
}
|
||||
|
||||
void _scrollToGroupSelected(String groupName) {
|
||||
void _scrollToGroupSelected(String groupName, int columns) {
|
||||
final currentInitOffset = _getGroupOffset(groupName);
|
||||
final currentGroups = getCurrentGroups();
|
||||
final proxies = currentGroups.getGroup(groupName)?.all;
|
||||
@@ -264,6 +222,7 @@ class _ProxiesListViewState extends State<ProxiesListView> {
|
||||
getScrollToSelectedOffset(
|
||||
groupName: groupName,
|
||||
proxies: proxies ?? [],
|
||||
columns: columns,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -288,87 +247,55 @@ class _ProxiesListViewState extends State<ProxiesListView> {
|
||||
builder: (_, ref, _) {
|
||||
final state = ref.watch(proxiesListStateProvider);
|
||||
ref.watch(themeSettingProvider.select((state) => state.textScale));
|
||||
final proxiesLayout = ref.watch(
|
||||
proxiesStyleSettingProvider.select((state) => state.layout),
|
||||
);
|
||||
if (state.groups.isEmpty) {
|
||||
return NullStatus(
|
||||
illustration: const ProxyEmptyIllustration(),
|
||||
label: appLocalizations.nullTip(appLocalizations.proxies),
|
||||
);
|
||||
}
|
||||
final items = _buildItems(
|
||||
ref,
|
||||
groups: state.groups,
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
columns: state.columns,
|
||||
cardType: state.proxyCardType,
|
||||
);
|
||||
final itemsOffset = _getItemHeightList(items, state.proxyCardType);
|
||||
return CommonScrollBar(
|
||||
controller: _controller,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
final columns = utils.getProxiesColumns(
|
||||
max(constraints.maxWidth - 32, 0),
|
||||
proxiesLayout,
|
||||
);
|
||||
_groupOffsets = _getGroupOffsets(
|
||||
groups: state.groups,
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
columns: columns,
|
||||
cardType: state.proxyCardType,
|
||||
);
|
||||
containerHeight = max(constraints.maxHeight - 16, 0);
|
||||
return CommonScrollBar(
|
||||
controller: _controller,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: ScrollConfiguration(
|
||||
behavior: HiddenBarScrollBehavior(),
|
||||
child: ListView.builder(
|
||||
child: CustomScrollView(
|
||||
key: proxiesListStoreKey,
|
||||
padding: const EdgeInsets.all(16),
|
||||
controller: _controller,
|
||||
itemExtentBuilder: (index, _) {
|
||||
return itemsOffset[index];
|
||||
},
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
slivers: [
|
||||
for (final group in state.groups)
|
||||
_buildGroup(
|
||||
context,
|
||||
group: group,
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
columns: columns,
|
||||
cardType: state.proxyCardType,
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
containerHeight = container.maxHeight;
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _headerStateNotifier,
|
||||
builder: (_, headerState, _) {
|
||||
if (headerState == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final index =
|
||||
headerState.currentIndex > state.groups.length - 1
|
||||
? 0
|
||||
: headerState.currentIndex;
|
||||
if (index < 0 || state.groups.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -headerState.offset,
|
||||
child: Container(
|
||||
width: container.maxWidth,
|
||||
color: context.colorScheme.surface,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 8,
|
||||
),
|
||||
child: _buildHeader(
|
||||
ref,
|
||||
group: state.groups[index],
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -471,6 +398,7 @@ class _ListHeaderState extends State<ListHeader> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
enterActionsOnRight: true,
|
||||
enterAnimated: widget.enterAnimated,
|
||||
key: widget.key,
|
||||
radius: 18.ap,
|
||||
|
||||
+63
-46
@@ -2,7 +2,6 @@ import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/clash_config.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -55,14 +54,31 @@ class ProxiesTabViewState extends ConsumerState<ProxiesTabView>
|
||||
}
|
||||
|
||||
void scrollToGroupSelected() {
|
||||
final currentGroupName = getCurrentGroupName();
|
||||
_keyMap[currentGroupName]?.currentState?.scrollToSelected();
|
||||
final group = currentGroup;
|
||||
if (group == null) {
|
||||
return;
|
||||
}
|
||||
_keyMap[group.name]?.currentState?.scrollToSelected();
|
||||
}
|
||||
|
||||
Future<void> delayTestCurrentGroup() async {
|
||||
final currentGroupName = getCurrentGroupName();
|
||||
final currentState = _keyMap[currentGroupName]?.currentState;
|
||||
await delayTest(currentState?.currentProxies ?? [], currentState?.testUrl);
|
||||
final group = currentGroup;
|
||||
if (group == null) {
|
||||
return;
|
||||
}
|
||||
await delayTest(group.all, group.testUrl);
|
||||
}
|
||||
|
||||
Group? get currentGroup {
|
||||
return _getGroup(_tabController?.index);
|
||||
}
|
||||
|
||||
Group? _getGroup(int? index) {
|
||||
final groups = ref.read(proxiesTabStateProvider).groups;
|
||||
if (index == null || index < 0 || index >= groups.length) {
|
||||
return null;
|
||||
}
|
||||
return groups[index];
|
||||
}
|
||||
|
||||
Widget _buildMoreButton() {
|
||||
@@ -126,21 +142,15 @@ class ProxiesTabViewState extends ConsumerState<ProxiesTabView>
|
||||
}
|
||||
|
||||
void _tabControllerListener([int? index]) {
|
||||
final group = _getGroup(index ?? _tabController?.index);
|
||||
if (group == null) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
int? groupIndex = index;
|
||||
if (groupIndex == -1) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (groupIndex == null) {
|
||||
final currentIndex = _tabController?.index;
|
||||
groupIndex = currentIndex;
|
||||
}
|
||||
final currentGroups = getCurrentGroups();
|
||||
if (groupIndex == null || groupIndex > currentGroups.length) {
|
||||
return;
|
||||
}
|
||||
final currentGroup = currentGroups[groupIndex];
|
||||
updateCurrentGroupName(currentGroup.name);
|
||||
updateCurrentGroupName(group.name);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -170,6 +180,9 @@ class ProxiesTabViewState extends ConsumerState<ProxiesTabView>
|
||||
final appLocalizations = context.appLocalizations;
|
||||
ref.watch(themeSettingProvider.select((state) => state.textScale));
|
||||
final state = ref.watch(proxiesTabStateProvider.select((state) => state));
|
||||
final proxiesLayout = ref.watch(
|
||||
proxiesStyleSettingProvider.select((state) => state.layout),
|
||||
);
|
||||
final groups = state.groups;
|
||||
if (groups.isEmpty || _tabController == null) {
|
||||
return NullStatus(
|
||||
@@ -203,9 +216,6 @@ class ProxiesTabViewState extends ConsumerState<ProxiesTabView>
|
||||
dividerColor: Colors.transparent,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
overlayColor: const WidgetStatePropertyAll(
|
||||
Colors.transparent,
|
||||
),
|
||||
tabs: [
|
||||
for (final group in groups)
|
||||
Tab(
|
||||
@@ -241,20 +251,28 @@ class ProxiesTabViewState extends ConsumerState<ProxiesTabView>
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
for (final group in groups)
|
||||
ProxyGroupView(
|
||||
key: _keyMap.updateCacheValue(
|
||||
group.name,
|
||||
() => GlobalObjectKey<_ProxyGroupViewState>(group.name),
|
||||
),
|
||||
group: group,
|
||||
columns: state.columns,
|
||||
cardType: state.proxyCardType,
|
||||
),
|
||||
],
|
||||
child: LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
final columns = utils.getProxiesColumns(
|
||||
max(constraints.maxWidth - 32, 0),
|
||||
proxiesLayout,
|
||||
);
|
||||
return TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
for (final group in groups)
|
||||
ProxyGroupView(
|
||||
key: _keyMap.updateCacheValue(
|
||||
group.name,
|
||||
() => GlobalObjectKey<_ProxyGroupViewState>(group.name),
|
||||
),
|
||||
group: group,
|
||||
columns: columns,
|
||||
cardType: state.proxyCardType,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -281,9 +299,6 @@ class ProxyGroupView extends ConsumerStatefulWidget {
|
||||
class _ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
|
||||
late final ScrollController _controller;
|
||||
|
||||
List<Proxy> currentProxies = [];
|
||||
String? testUrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -315,7 +330,8 @@ class _ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
|
||||
16 +
|
||||
getScrollToSelectedOffset(
|
||||
groupName: widget.group.name,
|
||||
proxies: currentProxies,
|
||||
proxies: widget.group.all,
|
||||
columns: widget.columns,
|
||||
),
|
||||
_controller.position.maxScrollExtent,
|
||||
),
|
||||
@@ -328,8 +344,6 @@ class _ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
|
||||
Widget build(BuildContext context) {
|
||||
final group = widget.group;
|
||||
final proxies = group.all;
|
||||
testUrl = group.testUrl;
|
||||
currentProxies = proxies;
|
||||
return CommonScrollBar(
|
||||
controller: _controller,
|
||||
child: GridView.builder(
|
||||
@@ -347,9 +361,9 @@ class _ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: getItemHeight(widget.cardType),
|
||||
),
|
||||
itemCount: currentProxies.length,
|
||||
itemCount: proxies.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = currentProxies[index];
|
||||
final proxy = proxies[index];
|
||||
return ProxyCard(
|
||||
testUrl: group.testUrl,
|
||||
groupType: group.type,
|
||||
@@ -382,9 +396,12 @@ class _DelayTestButtonState extends State<DelayTestButton>
|
||||
return;
|
||||
}
|
||||
_controller.forward();
|
||||
await widget.onClick();
|
||||
if (mounted) {
|
||||
_controller.reverse();
|
||||
try {
|
||||
await widget.onClick();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
_controller.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-2
@@ -2,6 +2,7 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'fade_box.dart';
|
||||
import 'text.dart';
|
||||
@@ -89,16 +90,18 @@ class CommonCard extends StatelessWidget {
|
||||
this.onPressed,
|
||||
this.selectWidget,
|
||||
this.radius,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.enterAnimated = false,
|
||||
this.info,
|
||||
this.onLongPress,
|
||||
this.shape,
|
||||
this.isError = false,
|
||||
this.enterActionsOnRight = false,
|
||||
required this.child,
|
||||
}) : isSelected = isSelected ?? false;
|
||||
|
||||
final bool enterAnimated;
|
||||
final bool enterActionsOnRight;
|
||||
final bool isSelected;
|
||||
final bool isError;
|
||||
final void Function()? onPressed;
|
||||
@@ -222,7 +225,7 @@ class CommonCard extends StatelessWidget {
|
||||
childWidget = Stack(children: children);
|
||||
}
|
||||
|
||||
final card = switch (type == CommonCardType.filled) {
|
||||
final button = switch (type == CommonCardType.filled) {
|
||||
true => FilledButton(
|
||||
onLongPress: onLongPress,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
@@ -275,6 +278,28 @@ class CommonCard extends StatelessWidget {
|
||||
child: childWidget,
|
||||
),
|
||||
};
|
||||
final card = !enterActionsOnRight
|
||||
? button
|
||||
: Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (_, event) {
|
||||
if (event is! KeyDownEvent ||
|
||||
event.logicalKey != LogicalKeyboardKey.arrowRight) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final focusNode = FocusManager.instance.primaryFocus;
|
||||
final context = focusNode?.context;
|
||||
if (focusNode == null ||
|
||||
context == null ||
|
||||
context.findAncestorWidgetOfExactType<IconButton>() != null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
return focusNode.nextFocus()
|
||||
? KeyEventResult.handled
|
||||
: KeyEventResult.ignored;
|
||||
},
|
||||
child: button,
|
||||
);
|
||||
|
||||
return switch (enterAnimated) {
|
||||
true => FadeScaleEnterBox(child: card),
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PageFocusScope extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const PageFocusScope({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<PageFocusScope> createState() => _PageFocusScopeState();
|
||||
}
|
||||
|
||||
class _PageFocusScopeState extends State<PageFocusScope> {
|
||||
final FocusScopeNode _node = FocusScopeNode()
|
||||
..traversalEdgeBehavior = TraversalEdgeBehavior.parentScope
|
||||
..directionalTraversalEdgeBehavior = TraversalEdgeBehavior.parentScope;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_node.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FocusScope.withExternalFocusNode(
|
||||
focusScopeNode: _node,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PageTraversalPolicy extends ReadingOrderTraversalPolicy {
|
||||
@override
|
||||
bool inDirection(FocusNode currentNode, TraversalDirection direction) {
|
||||
final scope = currentNode.nearestScope;
|
||||
if (super.inDirection(currentNode, direction)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final parent = scope?.enclosingScope;
|
||||
if (parent == null || parent == FocusManager.instance.rootScope) {
|
||||
return false;
|
||||
}
|
||||
return switch (direction) {
|
||||
TraversalDirection.down || TraversalDirection.right => parent.nextFocus(),
|
||||
TraversalDirection.up ||
|
||||
TraversalDirection.left => parent.previousFocus(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+218
-187
@@ -1,4 +1,3 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
@@ -10,66 +9,71 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'card.dart';
|
||||
import 'input.dart';
|
||||
import 'open_container.dart';
|
||||
import 'scaffold.dart';
|
||||
import 'sheet.dart';
|
||||
|
||||
class Delegate {
|
||||
const Delegate();
|
||||
sealed class _ListItemAction {
|
||||
const _ListItemAction();
|
||||
}
|
||||
|
||||
class RadioDelegate<T> extends Delegate {
|
||||
final class _DefaultAction extends _ListItemAction {
|
||||
const _DefaultAction();
|
||||
}
|
||||
|
||||
final class _RadioAction<T> extends _ListItemAction {
|
||||
final T value;
|
||||
final void Function()? onTab;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const RadioDelegate({required this.value, this.onTab});
|
||||
const _RadioAction({required this.value, this.onTap});
|
||||
}
|
||||
|
||||
class SwitchDelegate<T> extends Delegate {
|
||||
final class _ToggleAction extends _ListItemAction {
|
||||
final bool value;
|
||||
final ValueChanged<bool>? onChanged;
|
||||
|
||||
const SwitchDelegate({required this.value, this.onChanged});
|
||||
const _ToggleAction({required this.value, this.onChanged});
|
||||
}
|
||||
|
||||
class CheckboxDelegate<T> extends Delegate {
|
||||
final class _CheckboxAction extends _ListItemAction {
|
||||
final bool value;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
|
||||
const CheckboxDelegate({this.value = false, this.onChanged});
|
||||
const _CheckboxAction({required this.value, this.onChanged});
|
||||
}
|
||||
|
||||
class OpenDelegate<T> extends Delegate {
|
||||
final class _OpenAction extends _ListItemAction {
|
||||
final Widget widget;
|
||||
final double? maxWidth;
|
||||
final bool blur;
|
||||
final bool forceFull;
|
||||
final ValueChanged<T?>? onChanged;
|
||||
final ValueChanged<dynamic>? onChanged;
|
||||
|
||||
const OpenDelegate({
|
||||
const _OpenAction({
|
||||
required this.widget,
|
||||
this.maxWidth,
|
||||
this.blur = true,
|
||||
this.forceFull = true,
|
||||
required this.blur,
|
||||
required this.forceFull,
|
||||
this.onChanged,
|
||||
});
|
||||
}
|
||||
|
||||
class NextDelegate extends Delegate {
|
||||
final class _NextAction extends _ListItemAction {
|
||||
final Widget widget;
|
||||
final double? maxWidth;
|
||||
final bool blur;
|
||||
|
||||
const NextDelegate({required this.widget, this.maxWidth, this.blur = true});
|
||||
const _NextAction({required this.widget, this.maxWidth, required this.blur});
|
||||
}
|
||||
|
||||
class OptionsDelegate<T> extends Delegate {
|
||||
final class _OptionsAction<T> extends _ListItemAction {
|
||||
final List<T> options;
|
||||
final String title;
|
||||
final T value;
|
||||
final String Function(T value) textBuilder;
|
||||
final Function(T? value) onChanged;
|
||||
final ValueChanged<T?> onChanged;
|
||||
|
||||
const OptionsDelegate({
|
||||
const _OptionsAction({
|
||||
required this.title,
|
||||
required this.options,
|
||||
required this.textBuilder,
|
||||
@@ -78,18 +82,17 @@ class OptionsDelegate<T> extends Delegate {
|
||||
});
|
||||
}
|
||||
|
||||
class InputDelegate extends Delegate {
|
||||
final class _InputAction extends _ListItemAction {
|
||||
final String title;
|
||||
final String value;
|
||||
final String? suffixText;
|
||||
final Function(String? value) onChanged;
|
||||
final ValueChanged<String?> onChanged;
|
||||
final FormFieldValidator<String>? validator;
|
||||
final int? maxLength;
|
||||
final TextInputType? keyboardType;
|
||||
|
||||
final String? resetValue;
|
||||
|
||||
const InputDelegate({
|
||||
const _InputAction({
|
||||
required this.title,
|
||||
required this.value,
|
||||
this.suffixText,
|
||||
@@ -109,7 +112,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
final ListTileTitleAlignment tileTitleAlignment;
|
||||
final bool? dense;
|
||||
final Widget? trailing;
|
||||
final Delegate delegate;
|
||||
final _ListItemAction _action;
|
||||
final double? horizontalTitleGap;
|
||||
final TextStyle? titleTextStyle;
|
||||
final TextStyle? subtitleTextStyle;
|
||||
@@ -136,16 +139,20 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.visualDensity,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : delegate = const Delegate();
|
||||
}) : _action = const _DefaultAction();
|
||||
|
||||
const ListItem.open({
|
||||
ListItem.open({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
||||
this.trailing,
|
||||
required OpenDelegate this.delegate,
|
||||
required Widget widget,
|
||||
double? maxWidth,
|
||||
bool blur = true,
|
||||
bool forceFull = true,
|
||||
ValueChanged<dynamic>? onChanged,
|
||||
this.horizontalTitleGap,
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
@@ -155,16 +162,25 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.visualDensity,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
}) : _action = _OpenAction(
|
||||
widget: widget,
|
||||
maxWidth: maxWidth,
|
||||
blur: blur,
|
||||
forceFull: forceFull,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
onTap = null;
|
||||
|
||||
const ListItem.next({
|
||||
ListItem.next({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
||||
this.trailing,
|
||||
required NextDelegate this.delegate,
|
||||
required Widget widget,
|
||||
double? maxWidth,
|
||||
bool blur = true,
|
||||
this.horizontalTitleGap,
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
@@ -174,16 +190,21 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.visualDensity,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
}) : _action = _NextAction(widget: widget, maxWidth: maxWidth, blur: blur),
|
||||
onTap = null;
|
||||
|
||||
const ListItem.options({
|
||||
ListItem.options({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
||||
this.trailing,
|
||||
required OptionsDelegate<T> this.delegate,
|
||||
required String dialogTitle,
|
||||
required List<T> options,
|
||||
required T value,
|
||||
required String Function(T value) textBuilder,
|
||||
required ValueChanged<T?> onChanged,
|
||||
this.horizontalTitleGap,
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
@@ -193,16 +214,30 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.visualDensity,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
}) : _action = _OptionsAction<T>(
|
||||
title: dialogTitle,
|
||||
options: options,
|
||||
value: value,
|
||||
textBuilder: textBuilder,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
onTap = null;
|
||||
|
||||
const ListItem.input({
|
||||
ListItem.input({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
||||
this.trailing,
|
||||
required InputDelegate this.delegate,
|
||||
required String dialogTitle,
|
||||
required String value,
|
||||
String? suffixText,
|
||||
required ValueChanged<String?> onChanged,
|
||||
FormFieldValidator<String>? validator,
|
||||
int? maxLength,
|
||||
TextInputType? keyboardType,
|
||||
String? resetValue,
|
||||
this.horizontalTitleGap,
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
@@ -212,15 +247,26 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.visualDensity,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
}) : _action = _InputAction(
|
||||
title: dialogTitle,
|
||||
value: value,
|
||||
suffixText: suffixText,
|
||||
onChanged: onChanged,
|
||||
validator: validator,
|
||||
maxLength: maxLength,
|
||||
keyboardType: keyboardType,
|
||||
resetValue: resetValue,
|
||||
),
|
||||
onTap = null;
|
||||
|
||||
const ListItem.checkbox({
|
||||
ListItem.checkbox({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.padding = const EdgeInsets.only(left: 16, right: 8),
|
||||
required CheckboxDelegate<T> this.delegate,
|
||||
bool value = false,
|
||||
ValueChanged<bool?>? onChanged,
|
||||
this.horizontalTitleGap,
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
@@ -230,16 +276,18 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.visualDensity,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : trailing = null,
|
||||
}) : _action = _CheckboxAction(value: value, onChanged: onChanged),
|
||||
trailing = null,
|
||||
onTap = null;
|
||||
|
||||
const ListItem.switchItem({
|
||||
ListItem.toggle({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.padding = const EdgeInsets.only(left: 16, right: 8),
|
||||
required SwitchDelegate<T> this.delegate,
|
||||
required bool value,
|
||||
ValueChanged<bool>? onChanged,
|
||||
this.horizontalTitleGap,
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
@@ -249,16 +297,18 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.visualDensity,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : trailing = null,
|
||||
}) : _action = _ToggleAction(value: value, onChanged: onChanged),
|
||||
trailing = null,
|
||||
onTap = null;
|
||||
|
||||
const ListItem.radio({
|
||||
ListItem.radio({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.trailing,
|
||||
this.padding = const EdgeInsets.only(left: 12, right: 16),
|
||||
required RadioDelegate<T> this.delegate,
|
||||
required T value,
|
||||
VoidCallback? onTap,
|
||||
this.horizontalTitleGap = 8,
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
@@ -268,7 +318,8 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.visualDensity,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : leading = null,
|
||||
}) : _action = _RadioAction<T>(value: value, onTap: onTap),
|
||||
leading = null,
|
||||
onTap = null;
|
||||
|
||||
Widget _buildListTile({
|
||||
@@ -298,151 +349,131 @@ class ListItem<T> extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (delegate is OpenDelegate) {
|
||||
final openDelegate = delegate as OpenDelegate;
|
||||
final child = openDelegate.widget;
|
||||
final onChanged = openDelegate.onChanged;
|
||||
return OpenContainer<T>(
|
||||
closedColor: context.colorScheme.surface,
|
||||
openColor: context.colorScheme.surface,
|
||||
closedElevation: 0,
|
||||
openElevation: 0,
|
||||
openShape: const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
closedShape: const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
closedBuilder: (context, action) {
|
||||
Future<void> openAction() async {
|
||||
final isMobile = globalState.container.read(isMobileViewProvider);
|
||||
if (!isMobile || kDebugMode) {
|
||||
final res = await showExtend(
|
||||
context,
|
||||
props: ExtendProps(
|
||||
blur: openDelegate.blur,
|
||||
maxWidth: openDelegate.maxWidth,
|
||||
forceFull: openDelegate.forceFull,
|
||||
),
|
||||
builder: (_) {
|
||||
return child;
|
||||
},
|
||||
);
|
||||
if (onChanged != null) {
|
||||
onChanged(res);
|
||||
switch (_action) {
|
||||
case final _OpenAction openDelegate:
|
||||
final child = openDelegate.widget;
|
||||
final onChanged = openDelegate.onChanged;
|
||||
return OpenContainer<dynamic>(
|
||||
closedBuilder: (context, action) {
|
||||
Future<void> openAction() async {
|
||||
final isMobile = globalState.container.read(isMobileViewProvider);
|
||||
if (!isMobile || kDebugMode) {
|
||||
final res = await showExtend(
|
||||
context,
|
||||
props: ExtendProps(
|
||||
blur: openDelegate.blur,
|
||||
maxWidth: openDelegate.maxWidth,
|
||||
forceFull: openDelegate.forceFull,
|
||||
),
|
||||
builder: (_) {
|
||||
return child;
|
||||
},
|
||||
);
|
||||
if (onChanged != null) {
|
||||
onChanged(res);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
action();
|
||||
}
|
||||
action();
|
||||
}
|
||||
|
||||
return _buildListTile(onTap: openAction);
|
||||
},
|
||||
onClosed: onChanged,
|
||||
openBuilder: (_, action) {
|
||||
return child;
|
||||
},
|
||||
);
|
||||
}
|
||||
if (delegate is NextDelegate) {
|
||||
final nextDelegate = delegate as NextDelegate;
|
||||
final child = nextDelegate.widget;
|
||||
return _buildListTile(onTap: openAction);
|
||||
},
|
||||
onClosed: onChanged,
|
||||
openBuilder: (_, action) {
|
||||
return child;
|
||||
},
|
||||
);
|
||||
case final _NextAction nextDelegate:
|
||||
final child = nextDelegate.widget;
|
||||
|
||||
return _buildListTile(
|
||||
onTap: () {
|
||||
showExtend(
|
||||
context,
|
||||
props: ExtendProps(
|
||||
blur: nextDelegate.blur,
|
||||
maxWidth: nextDelegate.maxWidth,
|
||||
),
|
||||
builder: (_) {
|
||||
return child;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
return _buildListTile(
|
||||
onTap: () {
|
||||
showExtend(
|
||||
context,
|
||||
props: ExtendProps(
|
||||
blur: nextDelegate.blur,
|
||||
maxWidth: nextDelegate.maxWidth,
|
||||
),
|
||||
builder: (_) {
|
||||
return child;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
case final _OptionsAction options:
|
||||
final optionsDelegate = options as _OptionsAction<T>;
|
||||
return _buildListTile(
|
||||
onTap: () async {
|
||||
final value = await globalState.showCommonDialog<T>(
|
||||
child: OptionsDialog<T>(
|
||||
title: optionsDelegate.title,
|
||||
options: optionsDelegate.options,
|
||||
textBuilder: optionsDelegate.textBuilder,
|
||||
value: optionsDelegate.value,
|
||||
),
|
||||
);
|
||||
optionsDelegate.onChanged(value);
|
||||
},
|
||||
);
|
||||
case final _InputAction inputDelegate:
|
||||
return _buildListTile(
|
||||
onTap: () async {
|
||||
final value = await globalState.showCommonDialog<String>(
|
||||
child: InputDialog(
|
||||
title: inputDelegate.title,
|
||||
value: inputDelegate.value,
|
||||
suffixText: inputDelegate.suffixText,
|
||||
resetValue: inputDelegate.resetValue,
|
||||
inputFormatters: inputDelegate.maxLength == null
|
||||
? null
|
||||
: TextInputLimits.limit(inputDelegate.maxLength!),
|
||||
keyboardType: inputDelegate.keyboardType,
|
||||
validator: inputDelegate.validator,
|
||||
),
|
||||
);
|
||||
inputDelegate.onChanged(value);
|
||||
},
|
||||
);
|
||||
case final _CheckboxAction checkboxDelegate:
|
||||
return _buildListTile(
|
||||
onTap: checkboxDelegate.onChanged == null
|
||||
? null
|
||||
: () {
|
||||
checkboxDelegate.onChanged!(!checkboxDelegate.value);
|
||||
},
|
||||
trailing: CommonCheckBox(
|
||||
value: checkboxDelegate.value,
|
||||
onChanged: checkboxDelegate.onChanged,
|
||||
),
|
||||
);
|
||||
case final _ToggleAction toggleAction:
|
||||
return _buildListTile(
|
||||
onTap: toggleAction.onChanged == null
|
||||
? null
|
||||
: () {
|
||||
toggleAction.onChanged!(!toggleAction.value);
|
||||
},
|
||||
trailing: Switch(
|
||||
value: toggleAction.value,
|
||||
onChanged: toggleAction.onChanged,
|
||||
),
|
||||
);
|
||||
case final _RadioAction radio:
|
||||
final radioDelegate = radio as _RadioAction<T>;
|
||||
return _buildListTile(
|
||||
onTap: radioDelegate.onTap,
|
||||
leading: Radio<T>(
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
value: radioDelegate.value,
|
||||
toggleable: true,
|
||||
),
|
||||
trailing: trailing,
|
||||
);
|
||||
case _DefaultAction():
|
||||
return _buildListTile(onTap: onTap);
|
||||
}
|
||||
if (delegate is OptionsDelegate) {
|
||||
final optionsDelegate = delegate as OptionsDelegate<T>;
|
||||
return _buildListTile(
|
||||
onTap: () async {
|
||||
final value = await globalState.showCommonDialog<T>(
|
||||
child: OptionsDialog<T>(
|
||||
title: optionsDelegate.title,
|
||||
options: optionsDelegate.options,
|
||||
textBuilder: optionsDelegate.textBuilder,
|
||||
value: optionsDelegate.value,
|
||||
),
|
||||
);
|
||||
optionsDelegate.onChanged(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
if (delegate is InputDelegate) {
|
||||
final inputDelegate = delegate as InputDelegate;
|
||||
return _buildListTile(
|
||||
onTap: () async {
|
||||
final value = await globalState.showCommonDialog<String>(
|
||||
child: InputDialog(
|
||||
title: inputDelegate.title,
|
||||
value: inputDelegate.value,
|
||||
suffixText: inputDelegate.suffixText,
|
||||
resetValue: inputDelegate.resetValue,
|
||||
inputFormatters: inputDelegate.maxLength == null
|
||||
? null
|
||||
: TextInputLimits.limit(inputDelegate.maxLength!),
|
||||
keyboardType: inputDelegate.keyboardType,
|
||||
validator: inputDelegate.validator,
|
||||
),
|
||||
);
|
||||
inputDelegate.onChanged(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
if (delegate is CheckboxDelegate) {
|
||||
final checkboxDelegate = delegate as CheckboxDelegate;
|
||||
return _buildListTile(
|
||||
onTap: () {
|
||||
if (checkboxDelegate.onChanged != null) {
|
||||
checkboxDelegate.onChanged!(!checkboxDelegate.value);
|
||||
}
|
||||
},
|
||||
trailing: CommonCheckBox(
|
||||
value: checkboxDelegate.value,
|
||||
onChanged: checkboxDelegate.onChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (delegate is SwitchDelegate) {
|
||||
final switchDelegate = delegate as SwitchDelegate;
|
||||
return _buildListTile(
|
||||
onTap: () {
|
||||
if (switchDelegate.onChanged != null) {
|
||||
switchDelegate.onChanged!(!switchDelegate.value);
|
||||
}
|
||||
},
|
||||
trailing: Switch(
|
||||
value: switchDelegate.value,
|
||||
onChanged: switchDelegate.onChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (delegate is RadioDelegate) {
|
||||
final radioDelegate = delegate as RadioDelegate<T>;
|
||||
return _buildListTile(
|
||||
onTap: radioDelegate.onTab,
|
||||
leading: Radio<T>(
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
value: radioDelegate.value,
|
||||
toggleable: true,
|
||||
),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
return _buildListTile(onTap: onTap);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export 'activate_box.dart';
|
||||
export 'animate_grid.dart';
|
||||
export 'animated_visibility.dart';
|
||||
export 'builder.dart';
|
||||
export 'button.dart';
|
||||
export 'card.dart';
|
||||
@@ -13,6 +14,7 @@ export 'donut_chart.dart';
|
||||
export 'effect.dart';
|
||||
export 'fade_box.dart';
|
||||
export 'float_layout.dart';
|
||||
export 'focus.dart';
|
||||
export 'grid.dart';
|
||||
export 'icon.dart';
|
||||
export 'inherited.dart';
|
||||
|
||||
Reference in New Issue
Block a user