first commit
This commit is contained in:
217
lib/Screens/Products/Widgets/acnoo_multiple_select_dropdown.dart
Normal file
217
lib/Screens/Products/Widgets/acnoo_multiple_select_dropdown.dart
Normal file
@@ -0,0 +1,217 @@
|
||||
import 'dart:ui';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Widgets/selected_button.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
class AcnooMultiSelectDropdown<T> extends StatefulWidget {
|
||||
AcnooMultiSelectDropdown({
|
||||
super.key,
|
||||
this.decoration,
|
||||
this.menuItemStyleData,
|
||||
this.buttonStyleData,
|
||||
this.iconStyleData,
|
||||
this.dropdownStyleData,
|
||||
required this.items,
|
||||
this.values,
|
||||
this.onChanged,
|
||||
required this.labelText,
|
||||
}) : assert(
|
||||
items.isEmpty ||
|
||||
values == null ||
|
||||
items.where((item) {
|
||||
return values.contains(item.value);
|
||||
}).length ==
|
||||
values.length,
|
||||
"There should be exactly one item with [AcnooMultiSelectDropdown]'s value in the items list. "
|
||||
'Either zero or 2 or more [MultiSelectDropdownMenuItem]s were detected with the same value',
|
||||
);
|
||||
|
||||
final List<MultiSelectDropdownMenuItem<T?>> items;
|
||||
final List<T?>? values;
|
||||
final void Function(List<T>? values)? onChanged;
|
||||
|
||||
final InputDecoration? decoration;
|
||||
final MenuItemStyleData? menuItemStyleData;
|
||||
final ButtonStyleData? buttonStyleData;
|
||||
final IconStyleData? iconStyleData;
|
||||
final DropdownStyleData? dropdownStyleData;
|
||||
|
||||
final String labelText;
|
||||
|
||||
@override
|
||||
State<AcnooMultiSelectDropdown<T>> createState() => _AcnooMultiSelectDropdownState<T>();
|
||||
}
|
||||
|
||||
class _AcnooMultiSelectDropdownState<T> extends State<AcnooMultiSelectDropdown<T>> {
|
||||
bool isOpen = false;
|
||||
void listenMenuChange(bool value) {
|
||||
setState(() {
|
||||
isOpen = value;
|
||||
if (!value) {
|
||||
widget.onChanged?.call(
|
||||
selectedItems.map((e) => e.value!).toList(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
late List<MultiSelectDropdownMenuItem<T?>> selectedItems;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedItems = widget.items.where((element) => widget.values?.contains(element.value) ?? false).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
return DropdownButtonFormField2<T>(
|
||||
decoration: (widget.decoration ?? const InputDecoration()).copyWith(
|
||||
labelText: widget.labelText,
|
||||
hintText: '',
|
||||
),
|
||||
menuItemStyleData: widget.menuItemStyleData ?? const MenuItemStyleData(),
|
||||
buttonStyleData: widget.buttonStyleData ?? const ButtonStyleData(),
|
||||
iconStyleData: widget.iconStyleData ?? const IconStyleData(),
|
||||
dropdownStyleData: widget.dropdownStyleData ?? const DropdownStyleData(),
|
||||
onMenuStateChange: listenMenuChange,
|
||||
customButton: _buildCustomButton(context),
|
||||
items: widget.items.map((item) {
|
||||
return DropdownMenuItem<T>(
|
||||
value: item.value,
|
||||
enabled: false,
|
||||
child: _buildMenuItem(context, item, _theme),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (_) {},
|
||||
);
|
||||
}
|
||||
|
||||
// --------------- CHANGE IS HERE ---------------- //
|
||||
Widget _buildCustomButton(BuildContext context) {
|
||||
const _itemPadding = EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
);
|
||||
if (selectedItems.isEmpty) {
|
||||
final _iconWidget = widget.iconStyleData?.icon ?? Icon(Icons.keyboard_arrow_down_outlined);
|
||||
return Padding(
|
||||
padding: _itemPadding,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.decoration?.hintText ?? lang.S.of(context).selectItems,
|
||||
style: widget.decoration?.hintStyle,
|
||||
),
|
||||
_iconWidget,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ScrollConfiguration(
|
||||
behavior: const ScrollBehavior().copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.touch,
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: selectedItems.reversed.map((item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: SelectedItemButton(
|
||||
padding: _itemPadding,
|
||||
labelText: item.labelText,
|
||||
onTap: () {
|
||||
// 1. Remove item from local state
|
||||
setState(() {
|
||||
selectedItems.remove(item);
|
||||
});
|
||||
|
||||
// 2. Trigger the onChanged callback immediately
|
||||
widget.onChanged?.call(
|
||||
selectedItems.map((e) => e.value!).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// ----------------------------------------------- //
|
||||
|
||||
Widget _buildMenuItem(
|
||||
BuildContext context,
|
||||
MultiSelectDropdownMenuItem<T?> item,
|
||||
ThemeData _theme,
|
||||
) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, itemState) {
|
||||
final _isSelected = selectedItems.contains(item);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
_isSelected ? selectedItems.remove(item) : selectedItems.add(item);
|
||||
widget.onChanged?.call(
|
||||
selectedItems.map((e) => e.value!).toList(),
|
||||
);
|
||||
setState(() {});
|
||||
itemState(() {});
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 48),
|
||||
alignment: AlignmentDirectional.center,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(item.labelText),
|
||||
),
|
||||
if (_isSelected)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: kMainColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultiSelectDropdownMenuItem<T> {
|
||||
MultiSelectDropdownMenuItem({
|
||||
required this.labelText,
|
||||
this.value,
|
||||
});
|
||||
final String labelText;
|
||||
final T? value;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MultiSelectDropdownMenuItem<T> &&
|
||||
runtimeType == other.runtimeType &&
|
||||
labelText == other.labelText &&
|
||||
value == other.value;
|
||||
|
||||
@override
|
||||
int get hashCode => labelText.hashCode ^ value.hashCode;
|
||||
}
|
||||
228
lib/Screens/Products/Widgets/dropdown_styles.dart
Normal file
228
lib/Screens/Products/Widgets/dropdown_styles.dart
Normal file
@@ -0,0 +1,228 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
class AcnooDropdownStyle {
|
||||
AcnooDropdownStyle(this.context);
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
// Theme
|
||||
ThemeData get _theme => Theme.of(context);
|
||||
bool get _isDark => _theme.brightness == Brightness.dark;
|
||||
|
||||
// Button Style
|
||||
ButtonStyleData get buttonStyle => const ButtonStyleData(width: 0);
|
||||
|
||||
// Dropdown Style
|
||||
AcnooDropdownStyleData get dropdownStyle {
|
||||
return AcnooDropdownStyleData(
|
||||
maxHeight: 300,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: _theme.colorScheme.primaryContainer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Icon Style
|
||||
AcnooDropdownIconData get iconStyle {
|
||||
return AcnooDropdownIconData(
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: _isDark ? Colors.white : kMainColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Menu Style
|
||||
AcnooDropdownMenuItemStyleData get menuItemStyle {
|
||||
return AcnooDropdownMenuItemStyleData(
|
||||
overlayColor: WidgetStateProperty.all<Color>(
|
||||
kMainColor.withValues(alpha: 0.25),
|
||||
),
|
||||
selectedMenuItemBuilder: (context, child) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor.withValues(alpha: 0.125),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
MenuItemStyleData get multiSelectMenuItemStyle {
|
||||
return MenuItemStyleData(
|
||||
overlayColor: WidgetStateProperty.all<Color>(
|
||||
kMainColor.withValues(alpha: 0.25),
|
||||
),
|
||||
selectedMenuItemBuilder: (context, child) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor.withValues(alpha: 0.125),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
child,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: kMainColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Text Style
|
||||
TextStyle? get textStyle => _theme.textTheme.bodyLarge;
|
||||
|
||||
/*
|
||||
DropdownMenuItem<T> firstItem<T>({
|
||||
required String title,
|
||||
required String actionTitle,
|
||||
void Function()? onTap,
|
||||
T? value,
|
||||
bool enabled = false,
|
||||
}) {
|
||||
return DropdownMenuItem(
|
||||
value: value,
|
||||
enabled: enabled,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AcnooTextStyle.kSubtitleText.copyWith(
|
||||
fontSize: 16,
|
||||
color: AcnooAppColors.k03,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: actionTitle,
|
||||
recognizer: TapGestureRecognizer()..onTap = onTap,
|
||||
),
|
||||
style: AcnooTextStyle.kSubtitleText.copyWith(
|
||||
fontSize: 14,
|
||||
color: AcnooAppColors.kPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AcnooDropdownStyleData extends DropdownStyleData {
|
||||
const AcnooDropdownStyleData({
|
||||
super.maxHeight,
|
||||
super.width,
|
||||
super.padding,
|
||||
super.scrollPadding,
|
||||
super.decoration,
|
||||
super.elevation,
|
||||
super.direction,
|
||||
super.offset,
|
||||
super.isOverButton,
|
||||
super.useSafeArea,
|
||||
super.isFullScreen,
|
||||
super.useRootNavigator,
|
||||
super.scrollbarTheme,
|
||||
super.openInterval,
|
||||
});
|
||||
|
||||
AcnooDropdownStyleData copyWith({
|
||||
double? maxHeight,
|
||||
double? width,
|
||||
EdgeInsetsGeometry? padding,
|
||||
EdgeInsetsGeometry? scrollPadding,
|
||||
BoxDecoration? decoration,
|
||||
int? elevation,
|
||||
DropdownDirection? direction,
|
||||
Offset? offset,
|
||||
bool? isOverButton,
|
||||
bool? useSafeArea,
|
||||
bool? isFullScreen,
|
||||
bool? useRootNavigator,
|
||||
ScrollbarThemeData? scrollbarTheme,
|
||||
Interval? openInterval,
|
||||
}) {
|
||||
return AcnooDropdownStyleData(
|
||||
maxHeight: maxHeight ?? this.maxHeight,
|
||||
width: width ?? this.width,
|
||||
padding: padding ?? this.padding,
|
||||
scrollPadding: scrollPadding ?? this.scrollPadding,
|
||||
decoration: decoration ?? this.decoration,
|
||||
elevation: elevation ?? this.elevation,
|
||||
direction: direction ?? this.direction,
|
||||
offset: offset ?? this.offset,
|
||||
isOverButton: isOverButton ?? this.isOverButton,
|
||||
useSafeArea: useSafeArea ?? this.useSafeArea,
|
||||
isFullScreen: isFullScreen ?? this.useRootNavigator,
|
||||
useRootNavigator: useRootNavigator ?? this.useRootNavigator,
|
||||
scrollbarTheme: scrollbarTheme ?? this.scrollbarTheme,
|
||||
openInterval: openInterval ?? this.openInterval,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AcnooDropdownIconData extends IconStyleData {
|
||||
const AcnooDropdownIconData({
|
||||
super.icon,
|
||||
super.iconDisabledColor,
|
||||
super.iconEnabledColor,
|
||||
super.iconSize,
|
||||
super.openMenuIcon,
|
||||
});
|
||||
|
||||
AcnooDropdownIconData copyWith({
|
||||
Widget? icon,
|
||||
Color? iconDisabledColor,
|
||||
Color? iconEnabledColor,
|
||||
double? iconSize,
|
||||
Widget? openMenuIcon,
|
||||
}) {
|
||||
return AcnooDropdownIconData(
|
||||
icon: icon ?? this.icon,
|
||||
iconDisabledColor: iconDisabledColor ?? this.iconDisabledColor,
|
||||
iconEnabledColor: iconEnabledColor ?? this.iconEnabledColor,
|
||||
iconSize: iconSize ?? this.iconSize,
|
||||
openMenuIcon: openMenuIcon ?? this.openMenuIcon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AcnooDropdownMenuItemStyleData extends MenuItemStyleData {
|
||||
const AcnooDropdownMenuItemStyleData({
|
||||
super.customHeights,
|
||||
super.height,
|
||||
super.overlayColor,
|
||||
super.padding,
|
||||
super.selectedMenuItemBuilder,
|
||||
});
|
||||
|
||||
AcnooDropdownMenuItemStyleData copyWith({
|
||||
List<double>? customHeights,
|
||||
double? height,
|
||||
Color? overlayColor,
|
||||
EdgeInsetsGeometry? padding,
|
||||
Widget Function(BuildContext, Widget)? selectedMenuItemBuilder,
|
||||
}) {
|
||||
return AcnooDropdownMenuItemStyleData(
|
||||
customHeights: customHeights ?? this.customHeights,
|
||||
height: height ?? this.height,
|
||||
overlayColor: overlayColor != null ? WidgetStateProperty.all<Color?>(overlayColor) : this.overlayColor,
|
||||
selectedMenuItemBuilder: selectedMenuItemBuilder ?? this.selectedMenuItemBuilder,
|
||||
);
|
||||
}
|
||||
}
|
||||
58
lib/Screens/Products/Widgets/selected_button.dart
Normal file
58
lib/Screens/Products/Widgets/selected_button.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
class SelectedItemButton extends StatelessWidget {
|
||||
const SelectedItemButton({
|
||||
super.key,
|
||||
required this.labelText,
|
||||
this.padding,
|
||||
this.onTap,
|
||||
this.showCloseButton = true,
|
||||
});
|
||||
final String labelText;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final void Function()? onTap;
|
||||
final bool showCloseButton;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: padding ??
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: kDarkWhite,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: labelText,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
color: kTitleColor,
|
||||
),
|
||||
children: [
|
||||
if (showCloseButton)
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 12,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: TextStyle(color: _theme.colorScheme.onPrimary),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/Screens/Products/Widgets/text_field_label_wrappers.dart
Normal file
37
lib/Screens/Products/Widgets/text_field_label_wrappers.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
// 🐦 Flutter imports:
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
class TextFieldLabelWrapper extends StatelessWidget {
|
||||
const TextFieldLabelWrapper({
|
||||
super.key,
|
||||
this.labelText,
|
||||
this.label,
|
||||
this.labelStyle,
|
||||
required this.inputField,
|
||||
});
|
||||
final String? labelText;
|
||||
final Widget? label;
|
||||
final TextStyle? labelStyle;
|
||||
final Widget inputField;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Label
|
||||
if (label == null)
|
||||
Text(
|
||||
labelText ?? lang.S.of(context).enterLabelText,
|
||||
style: labelStyle ?? _theme.inputDecorationTheme.floatingLabelStyle,
|
||||
)
|
||||
else
|
||||
label!,
|
||||
const SizedBox(height: 8),
|
||||
inputField,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user