Optimize focus control

This commit is contained in:
chen08209
2026-08-05 10:56:03 +08:00
parent 51e24ef9de
commit 425d32b50d
15 changed files with 1798 additions and 700 deletions
+104 -87
View File
@@ -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;
},
),
),
),
),
],
],
),
);
}
}
+188 -89
View File
@@ -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),
),
),
],
),
),
),
);
+39 -41
View File
@@ -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,
),
),
),
),
),
+43 -45
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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),
+50
View File
@@ -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
View File
@@ -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);
}
}
+2
View File
@@ -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';