first commit

This commit is contained in:
benjibennn
2023-12-27 16:10:09 +08:00
commit 4f35362cf9
370 changed files with 108340 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
import 'dart:convert';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'lat_lng.dart';
import 'place.dart';
import 'uploaded_file.dart';
import '/backend/schema/enums/enums.dart';
import '/auth/custom_auth/auth_util.dart';
String formatDate(String date) {
DateTime parseDate = DateTime.parse(date);
DateFormat format = DateFormat('yyyy/MM/dd');
String formatedDate = format.format(parseDate);
return formatedDate;
}
String? convertToUpperCase(String? text) {
// Check if the input text is not null before converting to uppercase
if (text != null && text.isNotEmpty) {
List<String> words = text.split(' ');
for (int i = 0; i < words.length; i++) {
String word = words[i];
if (word.isNotEmpty) {
words[i] = word[0].toUpperCase() + word.substring(1);
}
}
return words.join(' ');
} else {
return null; // or return an empty string, depending on your use case
}
}
String? positionLabel(String? position) {
switch (position) {
//data to label
case 'director':
return 'Director';
case 'shareholder':
return 'Shareholder';
case 'company_secretary':
return 'Company Secretary';
//label to data
case 'Director':
return 'director';
case 'Shareholder':
return 'shareholder';
case 'Company Secretary':
return 'company_secretary';
default:
return position; // return the original value if not recognized
}
}
String formatTime(String timestamp) {
// Parse the timestamp
DateTime dateTime = DateTime.parse(timestamp).toLocal();
// Convert to the desired format (9:54)
String formattedTime = DateFormat.Hm().format(dateTime);
return formattedTime;
}
bool comparePermission(
List<int> getPermissionID,
int permissionID,
) {
// Function definition
bool isIntInList(int target, List<int> intList) {
return intList.contains(target);
}
int myTargetInt = permissionID;
List<int> myIntList = getPermissionID;
// Check if the list is empty, trigger an alert or return a different value
if (myIntList.isEmpty) {
// Trigger an alert or return a different value
// You can replace the following line with your alert logic or different return value
print('Alert: The permission list is empty!');
return false;
}
return isIntInList(myTargetInt, myIntList);
}
String formatDate2(String date) {
DateTime parseDate = DateTime.parse(date);
DateFormat format = DateFormat('yyyyMMdd');
String formatedDate = format.format(parseDate);
return formatedDate;
}
String getCategoryLabel(int categoryId) {
String label;
switch (categoryId) {
case 1:
label = 'Bank Statement';
break;
case 2:
label = 'Income';
break;
case 3:
label = 'Expense';
break;
case 4:
label = 'Others';
break;
case 5:
label = 'Income Tax Statement';
break;
default:
label =
'Unknown Category'; // Default label if category ID doesn't match any case
break;
}
return label;
}
Color getStatusColor(String status) {
switch (status) {
case "uploaded":
case "in_progress":
return Colors.white;
case "ocr_complete":
return Color(0xFFFFEFBD); // Yellow color for "ocr_complete"
case "failed":
return Color(0xFFF5AE97); // Red color for "failed"
default:
return Colors.white; // Default color if status doesn't match any case
}
}
List<String> filteredRemovePermissions(
List<String>? addPermissions,
List<String>? removePermissions,
) {
if (addPermissions == null || removePermissions == null) {
// Handle the case where one or both lists are null
return [];
}
// Create sets for faster lookup
Set<String> addSet = Set<String>.from(addPermissions);
Set<String> removeSet = Set<String>.from(removePermissions);
// Find the differing values
List<String> differingValues = removeSet.difference(addSet).toList();
// Print the differing values (you can replace this with your desired logic)
print('Differing values: $differingValues');
return differingValues;
}
List<dynamic> extractStatus(String jsonData) {
try {
final Map<String, dynamic> data = jsonDecode(jsonData);
if (data.containsKey('documents') && data['documents'] is List) {
List<dynamic> documents = data['documents'];
// Filter the documents with 'completed' status
List<Map<String, dynamic>> completedDocuments = documents
.whereType<Map<String, dynamic>>()
.where((doc) => doc['status'] == 'completed')
.toList();
// Extract necessary information
List<dynamic> extractedData = completedDocuments.map((doc) {
return {
'Category ID': doc['bookkeeping_document_category_id'],
'URL': doc['url'],
'Created At': doc['created_at']
};
}).toList();
return extractedData;
} else {
print('Invalid JSON format or missing "documents" key.');
return []; // Return an empty list if data is not as expected
}
} catch (e) {
print('Error: $e');
return []; // Return an empty list in case of an error
}
}
String getStatus2(String status) {
switch (status) {
case "uploaded":
return 'Uploaded'; // Blue color for "Uploaded"
case "in_progress":
return 'In progress'; // Blue color for "In progress"
case "ocr_complete":
return 'OCR complete'; // Yellow color for "OCR complete"
case "failed":
return 'Failed'; // Red color for "Failed"
default:
return '::0xFFFFFFFF'; // Default color if status doesn't match any case (white color)
}
}
List<String> addAccessRightPrefix(List<dynamic> roles) {
List<String> result = [];
for (String item in roles) {
String newItem = "Access right : $item";
result.add(newItem);
}
return result;
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
enum AnimationTrigger {
onPageLoad,
onActionTrigger,
}
class AnimationInfo {
AnimationInfo({
required this.trigger,
required this.effects,
this.loop = false,
this.reverse = false,
this.applyInitialState = true,
});
final AnimationTrigger trigger;
final List<Effect<dynamic>> effects;
final bool applyInitialState;
final bool loop;
final bool reverse;
late AnimationController controller;
}
void createAnimation(AnimationInfo animation, TickerProvider vsync) {
final newController = AnimationController(vsync: vsync);
animation.controller = newController;
}
void setupAnimations(Iterable<AnimationInfo> animations, TickerProvider vsync) {
animations.forEach((animation) => createAnimation(animation, vsync));
}
extension AnimatedWidgetExtension on Widget {
Widget animateOnPageLoad(AnimationInfo animationInfo) => Animate(
effects: animationInfo.effects,
child: this,
onPlay: (controller) => animationInfo.loop
? controller.repeat(reverse: animationInfo.reverse)
: null,
onComplete: (controller) => !animationInfo.loop && animationInfo.reverse
? controller.reverse()
: null);
Widget animateOnActionTrigger(
AnimationInfo animationInfo, {
bool hasBeenTriggered = false,
}) =>
hasBeenTriggered || animationInfo.applyInitialState
? Animate(
controller: animationInfo.controller,
autoPlay: false,
effects: animationInfo.effects,
child: this)
: this;
}
class TiltEffect extends Effect<Offset> {
const TiltEffect({
Duration? delay,
Duration? duration,
Curve? curve,
Offset? begin,
Offset? end,
}) : super(
delay: delay,
duration: duration,
curve: curve,
begin: begin ?? const Offset(0.0, 0.0),
end: end ?? const Offset(0.0, 0.0),
);
@override
Widget build(
BuildContext context,
Widget child,
AnimationController controller,
EffectEntry entry,
) {
Animation<Offset> animation = buildAnimation(controller, entry);
return getOptimizedBuilder<Offset>(
animation: animation,
builder: (_, __) => Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(animation.value.dx)
..rotateY(animation.value.dy),
alignment: Alignment.center,
child: child,
),
);
}
}

View File

@@ -0,0 +1,865 @@
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
const double _kTabHeight = 46.0;
typedef _LayoutCallback = void Function(
List<double> xOffsets, TextDirection textDirection, double width);
class _TabLabelBarRenderer extends RenderFlex {
_TabLabelBarRenderer({
required Axis direction,
required MainAxisSize mainAxisSize,
required MainAxisAlignment mainAxisAlignment,
required CrossAxisAlignment crossAxisAlignment,
required TextDirection textDirection,
required VerticalDirection verticalDirection,
required this.onPerformLayout,
}) : super(
direction: direction,
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection,
verticalDirection: verticalDirection,
);
_LayoutCallback onPerformLayout;
@override
void performLayout() {
super.performLayout();
// xOffsets will contain childCount+1 values, giving the offsets of the
// leading edge of the first tab as the first value, of the leading edge of
// the each subsequent tab as each subsequent value, and of the trailing
// edge of the last tab as the last value.
RenderBox? child = firstChild;
final List<double> xOffsets = <double>[];
while (child != null) {
final FlexParentData childParentData =
child.parentData! as FlexParentData;
xOffsets.add(childParentData.offset.dx);
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
assert(textDirection != null);
switch (textDirection!) {
case TextDirection.rtl:
xOffsets.insert(0, size.width);
break;
case TextDirection.ltr:
xOffsets.add(size.width);
break;
}
onPerformLayout(xOffsets, textDirection!, size.width);
}
}
// This class and its renderer class only exist to report the widths of the tabs
// upon layout. The tab widths are only used at paint time (see _IndicatorPainter)
// or in response to input.
class _TabLabelBar extends Flex {
_TabLabelBar({
required List<Widget> children,
required this.onPerformLayout,
}) : super(
children: children,
direction: Axis.horizontal,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
verticalDirection: VerticalDirection.down,
);
final _LayoutCallback onPerformLayout;
@override
RenderFlex createRenderObject(BuildContext context) {
return _TabLabelBarRenderer(
direction: direction,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: getEffectiveTextDirection(context)!,
verticalDirection: verticalDirection,
onPerformLayout: onPerformLayout,
);
}
@override
void updateRenderObject(
BuildContext context, _TabLabelBarRenderer renderObject) {
super.updateRenderObject(context, renderObject);
renderObject.onPerformLayout = onPerformLayout;
}
}
class _IndicatorPainter extends CustomPainter {
_IndicatorPainter({
required this.controller,
required this.tabKeys,
required _IndicatorPainter? old,
}) : super(repaint: controller.animation) {
if (old != null) {
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
}
}
final TabController controller;
final List<GlobalKey> tabKeys;
// _currentTabOffsets and _currentTextDirection are set each time TabBar
// layout is completed. These values can be null when TabBar contains no
// tabs, since there are nothing to lay out.
List<double>? _currentTabOffsets;
TextDirection? _currentTextDirection;
BoxPainter? _painter;
bool _needsPaint = false;
void markNeedsPaint() {
_needsPaint = true;
}
void dispose() {
_painter?.dispose();
}
void saveTabOffsets(List<double>? tabOffsets, TextDirection? textDirection) {
_currentTabOffsets = tabOffsets;
_currentTextDirection = textDirection;
}
// _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
// _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
int get maxTabIndex => _currentTabOffsets!.length - 2;
double centerOf(int tabIndex) {
assert(_currentTabOffsets != null);
assert(_currentTabOffsets!.isNotEmpty);
assert(tabIndex >= 0);
assert(tabIndex <= maxTabIndex);
return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) /
2.0;
}
@override
void paint(Canvas canvas, Size size) {
_needsPaint = false;
}
@override
bool shouldRepaint(_IndicatorPainter old) {
return _needsPaint ||
controller != old.controller ||
tabKeys.length != old.tabKeys.length ||
(!listEquals(_currentTabOffsets, old._currentTabOffsets)) ||
_currentTextDirection != old._currentTextDirection;
}
}
// This class, and TabBarScrollController, only exist to handle the case
// where a scrollable TabBar has a non-zero initialIndex. In that case we can
// only compute the scroll position's initial scroll offset (the "correct"
// pixels value) after the TabBar viewport width and scroll limits are known.
class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
_TabBarScrollPosition({
required ScrollPhysics physics,
required ScrollContext context,
required ScrollPosition? oldPosition,
required this.tabBar,
}) : super(
initialPixels: null,
physics: physics,
context: context,
oldPosition: oldPosition,
);
final _FlutterButtonTabBarState tabBar;
bool _viewportDimensionWasNonZero = false;
// Position should be adjusted at least once.
bool _needsPixelsCorrection = true;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
bool result = true;
if (!_viewportDimensionWasNonZero) {
_viewportDimensionWasNonZero = viewportDimension != 0.0;
}
// If the viewport never had a non-zero dimension, we just want to jump
// to the initial scroll position to avoid strange scrolling effects in
// release mode: In release mode, the viewport temporarily may have a
// dimension of zero before the actual dimension is calculated. In that
// scenario, setting the actual dimension would cause a strange scroll
// effect without this guard because the super call below would starts a
// ballistic scroll activity.
if (!_viewportDimensionWasNonZero || _needsPixelsCorrection) {
_needsPixelsCorrection = false;
correctPixels(tabBar._initialScrollOffset(
viewportDimension, minScrollExtent, maxScrollExtent));
result = false;
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent) &&
result;
}
void markNeedsPixelsCorrection() {
_needsPixelsCorrection = true;
}
}
// This class, and TabBarScrollPosition, only exist to handle the case
// where a scrollable TabBar has a non-zero initialIndex.
class _TabBarScrollController extends ScrollController {
_TabBarScrollController(this.tabBar);
final _FlutterButtonTabBarState tabBar;
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition? oldPosition) {
return _TabBarScrollPosition(
physics: physics,
context: context,
oldPosition: oldPosition,
tabBar: tabBar,
);
}
}
/// A flutter Design widget that displays a horizontal row of tabs.
class FlutterButtonTabBar extends StatefulWidget
implements PreferredSizeWidget {
/// The [tabs] argument must not be null and its length must match the [controller]'s
/// [TabController.length].
///
/// If a [TabController] is not provided, then there must be a
/// [DefaultTabController] ancestor.
///
const FlutterButtonTabBar({
Key? key,
required this.tabs,
this.controller,
this.isScrollable = false,
this.useToggleButtonStyle = false,
this.dragStartBehavior = DragStartBehavior.start,
this.onTap,
this.backgroundColor,
this.unselectedBackgroundColor,
this.decoration,
this.unselectedDecoration,
this.labelStyle,
this.unselectedLabelStyle,
this.labelColor,
this.unselectedLabelColor,
this.borderWidth = 0,
this.borderColor = Colors.transparent,
this.unselectedBorderColor = Colors.transparent,
this.physics = const BouncingScrollPhysics(),
this.labelPadding = const EdgeInsets.symmetric(horizontal: 4),
this.buttonMargin = const EdgeInsets.all(4),
this.padding = EdgeInsets.zero,
this.borderRadius = 8.0,
this.elevation = 0,
}) : super(key: key);
/// Typically a list of two or more [Tab] widgets.
///
/// The length of this list must match the [controller]'s [TabController.length]
/// and the length of the [TabBarView.children] list.
final List<Widget> tabs;
/// This widget's selection and animation state.
///
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
/// will be used.
final TabController? controller;
/// Whether this tab bar can be scrolled horizontally.
///
/// If [isScrollable] is true, then each tab is as wide as needed for its label
/// and the entire [FlutterButtonTabBar] is scrollable. Otherwise each tab gets an equal
/// share of the available space.
final bool isScrollable;
/// Whether the tab buttons should be styled as toggle buttons.
final bool useToggleButtonStyle;
/// The background [Color] of the button on its selected state.
final Color? backgroundColor;
/// The background [Color] of the button on its unselected state.
final Color? unselectedBackgroundColor;
/// The [BoxDecoration] of the button on its selected state.
///
/// If [BoxDecoration] is not provided, [backgroundColor] is used.
final BoxDecoration? decoration;
/// The [BoxDecoration] of the button on its unselected state.
///
/// If [BoxDecoration] is not provided, [unselectedBackgroundColor] is used.
final BoxDecoration? unselectedDecoration;
/// The [TextStyle] of the button's [Text] on its selected state. The color provided
/// on the TextStyle will be used for the [Icon]'s color.
final TextStyle? labelStyle;
/// The color of selected tab labels.
final Color? labelColor;
/// The color of unselected tab labels.
final Color? unselectedLabelColor;
/// The [TextStyle] of the button's [Text] on its unselected state. The color provided
/// on the TextStyle will be used for the [Icon]'s color.
final TextStyle? unselectedLabelStyle;
/// The with of solid [Border] for each button. If no value is provided, the border
/// is not drawn.
final double borderWidth;
/// The [Color] of solid [Border] for each button.
final Color? borderColor;
/// The [Color] of solid [Border] for each button. If no value is provided, the value of
/// [this.borderColor] is used.
final Color? unselectedBorderColor;
/// The [EdgeInsets] used for the [Padding] of the buttons' content.
///
/// The default value is [EdgeInsets.symmetric(horizontal: 4)].
final EdgeInsetsGeometry labelPadding;
/// The [EdgeInsets] used for the [Margin] of the buttons.
///
/// The default value is [EdgeInsets.all(4)].
final EdgeInsetsGeometry buttonMargin;
/// The amount of space by which to inset the tab bar.
final EdgeInsetsGeometry? padding;
/// The value of the [BorderRadius.circular] applied to each button.
final double borderRadius;
/// The value of the [elevation] applied to each button.
final double elevation;
final DragStartBehavior dragStartBehavior;
final ValueChanged<int>? onTap;
final ScrollPhysics? physics;
/// A size whose height depends on if the tabs have both icons and text.
///
/// [AppBar] uses this size to compute its own preferred size.
@override
Size get preferredSize {
double maxHeight = _kTabHeight;
for (final Widget item in tabs) {
if (item is PreferredSizeWidget) {
final double itemHeight = item.preferredSize.height;
maxHeight = math.max(itemHeight, maxHeight);
}
}
return Size.fromHeight(
maxHeight + labelPadding.vertical + buttonMargin.vertical);
}
@override
State<FlutterButtonTabBar> createState() =>
_FlutterButtonTabBarState();
}
class _FlutterButtonTabBarState extends State<FlutterButtonTabBar>
with TickerProviderStateMixin {
ScrollController? _scrollController;
TabController? _controller;
_IndicatorPainter? _indicatorPainter;
late AnimationController _animationController;
int _currentIndex = 0;
int _prevIndex = -1;
late double _tabStripWidth;
late List<GlobalKey> _tabKeys;
final GlobalKey _tabsParentKey = GlobalKey();
bool _debugHasScheduledValidTabsCountCheck = false;
@override
void initState() {
super.initState();
// If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find
// the width of tab widget i. See _IndicatorPainter.indicatorRect().
_tabKeys = widget.tabs.map((tab) => GlobalKey()).toList();
/// The animation duration is 2/3 of the tab scroll animation duration in
/// Material design (kTabScrollDuration).
_animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 200));
// so the buttons start in their "final" state (color)
_animationController
..value = 1.0
..addListener(() {
if (mounted) {
setState(() {});
}
});
}
// If the TabBar is rebuilt with a new tab controller, the caller should
// dispose the old one. In that case the old controller's animation will be
// null and should not be accessed.
bool get _controllerIsValid => _controller?.animation != null;
void _updateTabController() {
final TabController? newController =
widget.controller ?? DefaultTabController.maybeOf(context);
assert(() {
if (newController == null) {
throw FlutterError(
'No TabController for ${widget.runtimeType}.\n'
'When creating a ${widget.runtimeType}, you must either provide an explicit '
'TabController using the "controller" property, or you must ensure that there '
'is a DefaultTabController above the ${widget.runtimeType}.\n'
'In this case, there was neither an explicit controller nor a default controller.',
);
}
return true;
}());
if (newController == _controller) {
return;
}
if (_controllerIsValid) {
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
_controller!.removeListener(_handleTabControllerTick);
}
_controller = newController;
if (_controller != null) {
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
_controller!.addListener(_handleTabControllerTick);
_currentIndex = _controller!.index;
}
}
void _initIndicatorPainter() {
_indicatorPainter = !_controllerIsValid
? null
: _IndicatorPainter(
controller: _controller!,
tabKeys: _tabKeys,
old: _indicatorPainter,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
assert(debugCheckHasMaterial(context));
_updateTabController();
_initIndicatorPainter();
}
@override
void didUpdateWidget(FlutterButtonTabBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
_updateTabController();
_initIndicatorPainter();
// Adjust scroll position.
if (_scrollController != null) {
final ScrollPosition position = _scrollController!.position;
if (position is _TabBarScrollPosition) {
position.markNeedsPixelsCorrection();
}
}
}
if (widget.tabs.length > _tabKeys.length) {
final int delta = widget.tabs.length - _tabKeys.length;
_tabKeys.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey()));
} else if (widget.tabs.length < _tabKeys.length) {
_tabKeys.removeRange(widget.tabs.length, _tabKeys.length);
}
}
@override
void dispose() {
_indicatorPainter!.dispose();
if (_controllerIsValid) {
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
_controller!.removeListener(_handleTabControllerTick);
}
_controller = null;
// We don't own the _controller Animation, so it's not disposed here.
super.dispose();
}
int get maxTabIndex => _indicatorPainter!.maxTabIndex;
double _tabScrollOffset(
int index, double viewportWidth, double minExtent, double maxExtent) {
if (!widget.isScrollable) {
return 0.0;
}
double tabCenter = _indicatorPainter!.centerOf(index);
double paddingStart;
switch (Directionality.of(context)) {
case TextDirection.rtl:
paddingStart = widget.padding?.resolve(TextDirection.rtl).right ?? 0;
tabCenter = _tabStripWidth - tabCenter;
break;
case TextDirection.ltr:
paddingStart = widget.padding?.resolve(TextDirection.ltr).left ?? 0;
break;
}
return clampDouble(
tabCenter + paddingStart - viewportWidth / 2.0, minExtent, maxExtent);
}
double _tabCenteredScrollOffset(int index) {
final ScrollPosition position = _scrollController!.position;
return _tabScrollOffset(index, position.viewportDimension,
position.minScrollExtent, position.maxScrollExtent);
}
double _initialScrollOffset(
double viewportWidth, double minExtent, double maxExtent) {
return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent);
}
void _scrollToCurrentIndex() {
final double offset = _tabCenteredScrollOffset(_currentIndex);
_scrollController!
.animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);
}
void _scrollToControllerValue() {
final double? leadingPosition =
_currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null;
final double middlePosition = _tabCenteredScrollOffset(_currentIndex);
final double? trailingPosition = _currentIndex < maxTabIndex
? _tabCenteredScrollOffset(_currentIndex + 1)
: null;
final double index = _controller!.index.toDouble();
final double value = _controller!.animation!.value;
final double offset;
if (value == index - 1.0) {
offset = leadingPosition ?? middlePosition;
} else if (value == index + 1.0) {
offset = trailingPosition ?? middlePosition;
} else if (value == index) {
offset = middlePosition;
} else if (value < index) {
offset = leadingPosition == null
? middlePosition
: lerpDouble(middlePosition, leadingPosition, index - value)!;
} else {
offset = trailingPosition == null
? middlePosition
: lerpDouble(middlePosition, trailingPosition, value - index)!;
}
_scrollController!.jumpTo(offset);
}
void _handleTabControllerAnimationTick() {
assert(mounted);
if (!_controller!.indexIsChanging && widget.isScrollable) {
// Sync the TabBar's scroll position with the TabBarView's PageView.
_currentIndex = _controller!.index;
_scrollToControllerValue();
}
}
void _handleTabControllerTick() {
if (_controller!.index != _currentIndex) {
_prevIndex = _currentIndex;
_currentIndex = _controller!.index;
_triggerAnimation();
if (widget.isScrollable) {
_scrollToCurrentIndex();
}
}
setState(() {
// Rebuild the tabs after a (potentially animated) index change
// has completed.
});
}
void _triggerAnimation() {
// reset the animation so it's ready to go
_animationController
..reset()
..forward();
}
// Called each time layout completes.
void _saveTabOffsets(
List<double> tabOffsets, TextDirection textDirection, double width) {
_tabStripWidth = width;
_indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);
}
void _handleTap(int index) {
assert(index >= 0 && index < widget.tabs.length);
_controller?.animateTo(index);
widget.onTap?.call(index);
}
Widget _buildStyledTab(Widget child, int index) {
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final double animationValue;
if (index == _currentIndex) {
animationValue = _animationController.value;
} else if (index == _prevIndex) {
animationValue = 1 - _animationController.value;
} else {
animationValue = 0;
}
final TextStyle? textStyle = TextStyle.lerp(
(widget.unselectedLabelStyle ??
tabBarTheme.labelStyle ??
DefaultTextStyle.of(context).style)
.copyWith(
color: widget.unselectedLabelColor,
),
(widget.labelStyle ??
tabBarTheme.labelStyle ??
DefaultTextStyle.of(context).style)
.copyWith(
color: widget.labelColor,
),
animationValue);
final Color? textColor = Color.lerp(
widget.unselectedLabelColor, widget.labelColor, animationValue);
final Color? borderColor = Color.lerp(
widget.unselectedBorderColor, widget.borderColor, animationValue);
BoxDecoration? boxDecoration = BoxDecoration.lerp(
BoxDecoration(
color: widget.unselectedDecoration?.color ??
widget.unselectedBackgroundColor ??
Colors.transparent,
boxShadow: widget.unselectedDecoration?.boxShadow,
gradient: widget.unselectedDecoration?.gradient,
borderRadius: widget.useToggleButtonStyle
? null
: BorderRadius.circular(widget.borderRadius),
),
BoxDecoration(
color: widget.decoration?.color ??
widget.backgroundColor ??
Colors.transparent,
boxShadow: widget.decoration?.boxShadow,
gradient: widget.decoration?.gradient,
borderRadius: widget.useToggleButtonStyle
? null
: BorderRadius.circular(widget.borderRadius),
),
animationValue);
if (widget.useToggleButtonStyle &&
widget.borderWidth > 0 &&
boxDecoration != null) {
if (index == 0) {
boxDecoration = boxDecoration.copyWith(
border: Border(
right: BorderSide(
color: widget.unselectedBorderColor ?? Colors.transparent,
width: widget.borderWidth / 2,
),
),
);
} else if (index == widget.tabs.length - 1) {
boxDecoration = boxDecoration.copyWith(
border: Border(
left: BorderSide(
color: widget.unselectedBorderColor ?? Colors.transparent,
width: widget.borderWidth / 2,
),
),
);
} else {
boxDecoration = boxDecoration.copyWith(
border: Border.symmetric(
vertical: BorderSide(
color: widget.unselectedBorderColor ?? Colors.transparent,
width: widget.borderWidth / 2,
),
),
);
}
}
return Padding(
key: _tabKeys[index],
// padding for the buttons
padding:
widget.useToggleButtonStyle ? EdgeInsets.zero : widget.buttonMargin,
child: TextButton(
onPressed: () => _handleTap(index),
style: ButtonStyle(
elevation: MaterialStateProperty.all(
widget.useToggleButtonStyle ? 0 : widget.elevation),
/// give a pretty small minimum size
minimumSize: MaterialStateProperty.all(const Size(10, 10)),
padding: MaterialStateProperty.all(EdgeInsets.zero),
textStyle: MaterialStateProperty.all(textStyle),
foregroundColor: MaterialStateProperty.all(textColor),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: MaterialStateProperty.all(
widget.useToggleButtonStyle
? const RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: BorderRadius.zero,
)
: RoundedRectangleBorder(
side: (widget.borderWidth == 0)
? BorderSide.none
: BorderSide(
color: borderColor ?? Colors.transparent,
width: widget.borderWidth,
),
borderRadius: BorderRadius.circular(widget.borderRadius),
),
),
),
child: Ink(
decoration: boxDecoration,
child: Container(
padding: widget.labelPadding,
alignment: Alignment.center,
child: child,
),
),
),
);
}
bool _debugScheduleCheckHasValidTabsCount() {
if (_debugHasScheduledValidTabsCountCheck) {
return true;
}
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
_debugHasScheduledValidTabsCountCheck = false;
if (!mounted) {
return;
}
assert(() {
if (_controller!.length != widget.tabs.length) {
throw FlutterError(
"Controller's length property (${_controller!.length}) does not match the "
"number of tabs (${widget.tabs.length}) present in TabBar's tabs property.",
);
}
return true;
}());
});
_debugHasScheduledValidTabsCountCheck = true;
return true;
}
@override
Widget build(BuildContext context) {
assert(_debugScheduleCheckHasValidTabsCount());
if (_controller!.length == 0) {
return Container(
height: _kTabHeight +
widget.labelPadding.vertical +
widget.buttonMargin.vertical,
);
}
final List<Widget> wrappedTabs =
List<Widget>.generate(widget.tabs.length, (int index) {
return _buildStyledTab(widget.tabs[index], index);
});
final int tabCount = widget.tabs.length;
// Add the tap handler to each tab. If the tab bar is not scrollable,
// then give all of the tabs equal flexibility so that they each occupy
// the same share of the tab bar's overall width.
for (int index = 0; index < tabCount; index += 1) {
if (!widget.isScrollable) {
wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
}
}
Widget tabBar = AnimatedBuilder(
animation: _animationController,
key: _tabsParentKey,
builder: (context, child) {
Widget tabBarTemp = _TabLabelBar(
onPerformLayout: _saveTabOffsets,
children: wrappedTabs,
);
if (widget.useToggleButtonStyle) {
tabBarTemp = Material(
shape: widget.useToggleButtonStyle
? RoundedRectangleBorder(
side: (widget.borderWidth == 0)
? BorderSide.none
: BorderSide(
color: widget.borderColor ?? Colors.transparent,
width: widget.borderWidth,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(widget.borderRadius),
)
: null,
elevation: widget.useToggleButtonStyle ? widget.elevation : 0,
clipBehavior: Clip.antiAliasWithSaveLayer,
child: tabBarTemp,
);
}
return CustomPaint(
painter: _indicatorPainter,
child: tabBarTemp,
);
},
);
if (widget.isScrollable) {
_scrollController ??= _TabBarScrollController(this);
tabBar = SingleChildScrollView(
dragStartBehavior: widget.dragStartBehavior,
scrollDirection: Axis.horizontal,
controller: _scrollController,
padding: widget.padding,
physics: widget.physics,
child: tabBar,
);
} else if (widget.padding != null) {
tabBar = Padding(
padding: widget.padding!,
child: tabBar,
);
}
return tabBar;
}
}

View File

@@ -0,0 +1,183 @@
import 'form_field_controller.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '/flutterlib/flutter_util.dart';
class ChipData {
const ChipData(this.label, [this.iconData]);
final String label;
final IconData? iconData;
}
class ChipStyle {
const ChipStyle({
this.backgroundColor,
this.textStyle,
this.iconColor,
this.iconSize,
this.labelPadding,
this.elevation,
this.borderColor,
this.borderWidth,
this.borderRadius,
});
final Color? backgroundColor;
final TextStyle? textStyle;
final Color? iconColor;
final double? iconSize;
final EdgeInsetsGeometry? labelPadding;
final double? elevation;
final Color? borderColor;
final double? borderWidth;
final BorderRadius? borderRadius;
}
class FlutterChoiceChips extends StatefulWidget {
const FlutterChoiceChips({
super.key,
required this.options,
required this.onChanged,
required this.controller,
required this.selectedChipStyle,
required this.unselectedChipStyle,
required this.chipSpacing,
this.rowSpacing = 0.0,
required this.multiselect,
this.initialized = true,
this.alignment = WrapAlignment.start,
this.disabledColor,
this.wrapped = true,
});
final List<ChipData> options;
final void Function(List<String>?)? onChanged;
final FormFieldController<List<String>> controller;
final ChipStyle selectedChipStyle;
final ChipStyle unselectedChipStyle;
final double chipSpacing;
final double rowSpacing;
final bool multiselect;
final bool initialized;
final WrapAlignment alignment;
final Color? disabledColor;
final bool wrapped;
@override
State<FlutterChoiceChips> createState() => _FlutterChoiceChipsState();
}
class _FlutterChoiceChipsState extends State<FlutterChoiceChips> {
late List<String> choiceChipValues;
ValueListenable<List<String>?> get changeSelectedValues => widget.controller;
List<String> get selectedValues => widget.controller.value ?? [];
@override
void initState() {
super.initState();
choiceChipValues = selectedValues;
if (!widget.initialized && choiceChipValues.isNotEmpty) {
SchedulerBinding.instance.addPostFrameCallback(
(_) {
if (widget.onChanged != null) {
widget.onChanged!(choiceChipValues);
}
},
);
}
changeSelectedValues.addListener(() {
if (widget.onChanged != null) {
widget.onChanged!(selectedValues);
}
});
}
@override
void dispose() {
changeSelectedValues.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
final children = widget.options.map<Widget>(
(option) {
final selected = selectedValues.contains(option.label);
final style =
selected ? widget.selectedChipStyle : widget.unselectedChipStyle;
return Theme(
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
child: ChoiceChip(
selected: selected,
onSelected: widget.onChanged != null
? (isSelected) {
if (isSelected) {
widget.multiselect
? choiceChipValues.add(option.label)
: choiceChipValues = [option.label];
widget.controller.value = List.from(choiceChipValues);
setState(() {});
} else {
if (widget.multiselect) {
choiceChipValues.remove(option.label);
widget.controller.value = List.from(choiceChipValues);
setState(() {});
}
}
}
: null,
label: Text(
option.label,
style: style.textStyle,
),
labelPadding: style.labelPadding,
avatar: option.iconData != null
? FaIcon(
option.iconData,
size: style.iconSize,
color: style.iconColor,
)
: null,
elevation: style.elevation,
disabledColor: widget.disabledColor,
selectedColor:
selected ? widget.selectedChipStyle.backgroundColor : null,
backgroundColor:
selected ? null : widget.unselectedChipStyle.backgroundColor,
shape: RoundedRectangleBorder(
borderRadius: style.borderRadius ?? BorderRadius.circular(16),
side: BorderSide(
color: style.borderColor ?? Colors.transparent,
width: style.borderWidth ?? 0,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
);
},
).toList();
if (widget.wrapped) {
return Wrap(
spacing: widget.chipSpacing,
runSpacing: widget.rowSpacing,
alignment: widget.alignment,
crossAxisAlignment: WrapCrossAlignment.center,
children: children,
);
} else {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
clipBehavior: Clip.none,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: children.divide(
SizedBox(width: widget.chipSpacing),
),
),
);
}
}
}

View File

@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
class FlutterCountController extends StatefulWidget {
const FlutterCountController({
Key? key,
required this.decrementIconBuilder,
required this.incrementIconBuilder,
required this.countBuilder,
required this.count,
required this.updateCount,
this.stepSize = 1,
this.minimum,
this.maximum,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 25.0),
}) : super(key: key);
final Widget Function(bool enabled) decrementIconBuilder;
final Widget Function(bool enabled) incrementIconBuilder;
final Widget Function(int count) countBuilder;
final int count;
final Function(int) updateCount;
final int stepSize;
final int? minimum;
final int? maximum;
final EdgeInsetsGeometry contentPadding;
@override
_FlutterCountControllerState createState() =>
_FlutterCountControllerState();
}
class _FlutterCountControllerState
extends State<FlutterCountController> {
int get count => widget.count;
int? get minimum => widget.minimum;
int? get maximum => widget.maximum;
int get stepSize => widget.stepSize;
bool get canDecrement => minimum == null || count - stepSize >= minimum!;
bool get canIncrement => maximum == null || count + stepSize <= maximum!;
void _decrementCounter() {
if (canDecrement) {
setState(() => widget.updateCount(count - stepSize));
}
}
void _incrementCounter() {
if (canIncrement) {
setState(() => widget.updateCount(count + stepSize));
}
}
@override
Widget build(BuildContext context) => Padding(
padding: widget.contentPadding,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: _decrementCounter,
child: widget.decrementIconBuilder(canDecrement),
),
widget.countBuilder(count),
InkWell(
onTap: _incrementCounter,
child: widget.incrementIconBuilder(canIncrement),
),
],
),
);
}

View File

@@ -0,0 +1,339 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'form_field_controller.dart';
import 'package:flutter/material.dart';
class FlutterDropDown<T> extends StatefulWidget {
const FlutterDropDown({
Key? key,
required this.controller,
this.hintText,
this.searchHintText,
required this.options,
this.optionLabels,
this.onChanged,
this.onChangedForMultiSelect,
this.icon,
this.width,
this.height,
this.maxHeight,
this.fillColor,
this.searchHintTextStyle,
this.searchCursorColor,
required this.textStyle,
required this.elevation,
required this.borderWidth,
required this.borderRadius,
required this.borderColor,
required this.margin,
this.hidesUnderline = false,
this.disabled = false,
this.isOverButton = false,
this.isSearchable = false,
this.isMultiSelect = false,
}) : super(key: key);
final FormFieldController<T> controller;
final String? hintText;
final String? searchHintText;
final List<T> options;
final List<String>? optionLabels;
final Function(T?)? onChanged;
final Function(List<T>?)? onChangedForMultiSelect;
final Widget? icon;
final double? width;
final double? height;
final double? maxHeight;
final Color? fillColor;
final TextStyle? searchHintTextStyle;
final Color? searchCursorColor;
final TextStyle textStyle;
final double elevation;
final double borderWidth;
final double borderRadius;
final Color borderColor;
final EdgeInsetsGeometry margin;
final bool hidesUnderline;
final bool disabled;
final bool isOverButton;
final bool isSearchable;
final bool isMultiSelect;
@override
State<FlutterDropDown<T>> createState() => _FlutterDropDownState<T>();
}
class _FlutterDropDownState<T> extends State<FlutterDropDown<T>> {
final TextEditingController _textEditingController = TextEditingController();
void Function() get listener => widget.isMultiSelect
? () {}
: () => widget.onChanged!(widget.controller.value);
@override
void initState() {
widget.controller.addListener(listener);
super.initState();
}
@override
void dispose() {
widget.controller.removeListener(listener);
super.dispose();
}
List<T> selectedItems = [];
@override
Widget build(BuildContext context) {
final optionToDisplayValue = Map.fromEntries(
widget.options.asMap().entries.map((option) => MapEntry(
option.value,
widget.optionLabels == null ||
widget.optionLabels!.length < option.key + 1
? option.value.toString()
: widget.optionLabels![option.key])),
);
final value = widget.options.contains(widget.controller.value)
? widget.controller.value
: null;
final items = widget.options
.map(
(option) => DropdownMenuItem<T>(
value: option,
child: Text(
optionToDisplayValue[option] ?? '',
style: widget.textStyle,
),
),
)
.toList();
final hintText = widget.hintText != null
? Text(widget.hintText!, style: widget.textStyle)
: null;
void Function(T?)? onChanged = widget.disabled || widget.isMultiSelect
? null
: (value) => widget.controller.value = value;
final dropdownWidget = _useDropdown2()
? _buildDropdown(
value,
items,
onChanged,
hintText,
optionToDisplayValue,
widget.isSearchable,
widget.disabled,
widget.isMultiSelect,
widget.onChangedForMultiSelect,
)
: _buildLegacyDropdown(value, items, onChanged, hintText);
final childWidget = DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.borderRadius),
border: Border.all(
color: widget.borderColor,
width: widget.borderWidth,
),
color: widget.fillColor,
),
child: Padding(
padding: widget.margin,
child: widget.hidesUnderline
? DropdownButtonHideUnderline(child: dropdownWidget)
: dropdownWidget,
),
);
if (widget.height != null || widget.width != null) {
return Container(
width: widget.width,
height: widget.height,
child: childWidget,
);
}
return childWidget;
}
Widget _buildLegacyDropdown(
T? value,
List<DropdownMenuItem<T>>? items,
void Function(T?)? onChanged,
Text? hintText,
) {
return DropdownButton<T>(
value: value,
hint: hintText,
items: items,
elevation: widget.elevation.toInt(),
onChanged: onChanged,
icon: widget.icon,
isExpanded: true,
dropdownColor: widget.fillColor,
focusColor: Colors.transparent,
);
}
Widget _buildDropdown(
T? value,
List<DropdownMenuItem<T>>? items,
void Function(T?)? onChanged,
Text? hintText,
Map<T, String> optionLabels,
bool isSearchable,
bool disabled,
bool isMultiSelect,
Function(List<T>?)? onChangedForMultiSelect,
) {
final overlayColor = MaterialStateProperty.resolveWith<Color?>((states) =>
states.contains(MaterialState.focused) ? Colors.transparent : null);
final iconStyleData = widget.icon != null
? IconStyleData(icon: widget.icon!)
: const IconStyleData();
return DropdownButton2<T>(
value: isMultiSelect
? selectedItems.isEmpty
? null
: selectedItems.last
: value,
hint: hintText,
items: isMultiSelect
? widget.options.map((item) {
return DropdownMenuItem(
value: item,
// Disable default onTap to avoid closing menu when selecting an item
enabled: false,
child: StatefulBuilder(
builder: (context, menuSetState) {
final isSelected = selectedItems.contains(item);
return InkWell(
onTap: () {
isSelected
? selectedItems.remove(item)
: selectedItems.add(item);
onChangedForMultiSelect!(selectedItems);
//This rebuilds the StatefulWidget to update the button's text
setState(() {});
//This rebuilds the dropdownMenu Widget to update the check mark
menuSetState(() {});
},
child: Container(
height: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
if (isSelected)
const Icon(Icons.check_box_outlined)
else
const Icon(Icons.check_box_outline_blank),
const SizedBox(width: 16),
Expanded(
child: Text(
item as String,
style: widget.textStyle,
),
),
],
),
),
);
},
),
);
}).toList()
: items,
iconStyleData: iconStyleData,
buttonStyleData: ButtonStyleData(
elevation: widget.elevation.toInt(),
overlayColor: overlayColor,
),
menuItemStyleData: MenuItemStyleData(overlayColor: overlayColor),
dropdownStyleData: DropdownStyleData(
elevation: widget.elevation!.toInt(),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: widget.fillColor,
),
isOverButton: widget.isOverButton,
maxHeight: widget.maxHeight,
),
// onChanged is handled by the onChangedForMultiSelect function
onChanged: isMultiSelect
? disabled
? null
: (val) {}
: onChanged,
isExpanded: true,
selectedItemBuilder: isMultiSelect
? (context) {
return widget.options.map(
(item) {
return Container(
alignment: AlignmentDirectional.center,
child: Text(
selectedItems.join(', '),
style: const TextStyle(
fontSize: 14,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
),
);
},
).toList();
}
: null,
dropdownSearchData: isSearchable
? DropdownSearchData<T>(
searchController: _textEditingController,
searchInnerWidgetHeight: 50,
searchInnerWidget: Container(
height: 50,
padding: const EdgeInsets.only(
top: 8,
bottom: 4,
right: 8,
left: 8,
),
child: TextFormField(
expands: true,
maxLines: null,
controller: _textEditingController,
cursorColor: widget.searchCursorColor,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
hintText: widget.searchHintText,
hintStyle: widget.searchHintTextStyle,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
searchMatchFn: (item, searchValue) {
return (optionLabels[item.value] ?? '')
.toLowerCase()
.contains(searchValue.toLowerCase());
},
)
: null,
// This to clear the search value when you close the menu
onMenuStateChange: isSearchable
? (isOpen) {
if (!isOpen) {
_textEditingController.clear();
}
}
: null,
);
}
bool _useDropdown2() {
return widget.isMultiSelect ||
widget.isSearchable ||
!widget.isOverButton ||
widget.maxHeight != null;
}
}

View File

@@ -0,0 +1,167 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class FlutterIconButton extends StatefulWidget {
const FlutterIconButton({
Key? key,
required this.icon,
this.borderColor,
this.borderRadius,
this.borderWidth,
this.buttonSize,
this.fillColor,
this.disabledColor,
this.disabledIconColor,
this.hoverColor,
this.hoverIconColor,
this.onPressed,
this.showLoadingIndicator = false,
}) : super(key: key);
final Widget icon;
final double? borderRadius;
final double? buttonSize;
final Color? fillColor;
final Color? disabledColor;
final Color? disabledIconColor;
final Color? hoverColor;
final Color? hoverIconColor;
final Color? borderColor;
final double? borderWidth;
final bool showLoadingIndicator;
final Function()? onPressed;
@override
State<FlutterIconButton> createState() => _FlutterIconButtonState();
}
class _FlutterIconButtonState extends State<FlutterIconButton> {
bool loading = false;
late double? iconSize;
late Color? iconColor;
late Widget effectiveIcon;
@override
void initState() {
super.initState();
_updateIcon();
}
@override
void didUpdateWidget(FlutterIconButton oldWidget) {
super.didUpdateWidget(oldWidget);
_updateIcon();
}
void _updateIcon() {
final isFontAwesome = widget.icon is FaIcon;
if (isFontAwesome) {
FaIcon icon = widget.icon as FaIcon;
effectiveIcon = FaIcon(
icon.icon,
size: icon.size,
);
iconSize = icon.size;
iconColor = icon.color;
} else {
Icon icon = widget.icon as Icon;
effectiveIcon = Icon(
icon.icon,
size: icon.size,
);
iconSize = icon.size;
iconColor = icon.color;
}
}
@override
Widget build(BuildContext context) {
ButtonStyle style = ButtonStyle(
shape: MaterialStateProperty.resolveWith<OutlinedBorder>(
(states) {
return RoundedRectangleBorder(
borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
side: BorderSide(
color: widget.borderColor ?? Colors.transparent,
width: widget.borderWidth ?? 0,
),
);
},
),
iconColor: MaterialStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(MaterialState.disabled) &&
widget.disabledIconColor != null) {
return widget.disabledIconColor;
}
if (states.contains(MaterialState.hovered) &&
widget.hoverIconColor != null) {
return widget.hoverIconColor;
}
return iconColor;
},
),
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(MaterialState.disabled) &&
widget.disabledColor != null) {
return widget.disabledColor;
}
if (states.contains(MaterialState.hovered) &&
widget.hoverColor != null) {
return widget.hoverColor;
}
return widget.fillColor;
},
),
overlayColor: MaterialStateProperty.resolveWith<Color?>((states) {
if (states.contains(MaterialState.pressed)) {
return null;
}
return widget.hoverColor == null ? null : Colors.transparent;
}),
);
return SizedBox(
width: widget.buttonSize,
height: widget.buttonSize,
child: Theme(
data: Theme.of(context).copyWith(useMaterial3: true),
child: IgnorePointer(
ignoring: (widget.showLoadingIndicator && loading),
child: IconButton(
icon: (widget.showLoadingIndicator && loading)
? Container(
width: iconSize,
height: iconSize,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
iconColor ?? Colors.white,
),
),
)
: effectiveIcon,
onPressed: widget.onPressed == null
? null
: () async {
if (loading) {
return;
}
setState(() => loading = true);
try {
await widget.onPressed!();
} finally {
if (mounted) {
setState(() => loading = false);
}
}
},
splashRadius: widget.buttonSize,
style: style,
),
),
),
);
}
}

View File

@@ -0,0 +1,169 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
Widget wrapWithModel<T extends FlutterModel>({
required T model,
required Widget child,
required VoidCallback updateCallback,
bool updateOnChange = false,
}) {
// Set the component to optionally update the page on updates.
model.setOnUpdate(
onUpdate: updateCallback,
updateOnChange: updateOnChange,
);
// Models for components within a page will be disposed by the page's model,
// so we don't want the component widget to dispose them until the page is
// itself disposed.
model.disposeOnWidgetDisposal = false;
// Wrap in a Provider so that the model can be accessed by the component.
return Provider<T>.value(
value: model,
child: child,
);
}
T createModel<T extends FlutterModel>(
BuildContext context,
T Function() defaultBuilder,
) {
final model = context.read<T?>() ?? defaultBuilder();
model._init(context);
return model;
}
abstract class FlutterModel<W extends Widget> {
// Initialization methods
bool _isInitialized = false;
void initState(BuildContext context);
void _init(BuildContext context) {
if (!_isInitialized) {
initState(context);
_isInitialized = true;
}
if (context.widget is W) _widget = context.widget as W;
}
// The widget associated with this model. This is useful for accessing the
// parameters of the widget, for example.
W? _widget;
// This will always be non-null when used, but is nullable to allow us to
// dispose of the widget in the [dispose] method (for garbage collection).
W get widget => _widget!;
// Dispose methods
// Whether to dispose this model when the corresponding widget is
// disposed. By default this is true for pages and false for components,
// as page/component models handle the disposal of their children.
bool disposeOnWidgetDisposal = true;
void dispose();
void maybeDispose() {
if (disposeOnWidgetDisposal) {
dispose();
}
// Remove reference to widget for garbage collection purposes.
_widget = null;
}
// Whether to update the containing page / component on updates.
bool updateOnChange = false;
// Function to call when the model receives an update.
VoidCallback _updateCallback = () {};
void onUpdate() => updateOnChange ? _updateCallback() : () {};
FlutterModel setOnUpdate({
bool updateOnChange = false,
required VoidCallback onUpdate,
}) =>
this
.._updateCallback = onUpdate
..updateOnChange = updateOnChange;
// Update the containing page when this model received an update.
void updatePage(VoidCallback callback) {
callback();
_updateCallback();
}
}
class FlutterDynamicModels<T extends FlutterModel> {
FlutterDynamicModels(this.defaultBuilder);
final T Function() defaultBuilder;
final Map<String, T> _childrenModels = {};
final Map<String, int> _childrenIndexes = {};
Set<String>? _activeKeys;
T getModel(String uniqueKey, int index) {
_updateActiveKeys(uniqueKey);
_childrenIndexes[uniqueKey] = index;
return _childrenModels[uniqueKey] ??= defaultBuilder();
}
List<S> getValues<S>(S? Function(T) getValue) {
return _childrenIndexes.entries
// Sort keys by index.
.sorted((a, b) => a.value.compareTo(b.value))
.where((e) => _childrenModels[e.key] != null)
// Map each model to the desired value and return as list. In order
// to preserve index order, rather than removing null values we provide
// default values (for types with reasonable defaults).
.map((e) => getValue(_childrenModels[e.key]!) ?? _getDefaultValue<S>()!)
.toList();
}
S? getValueAtIndex<S>(int index, S? Function(T) getValue) {
final uniqueKey =
_childrenIndexes.entries.firstWhereOrNull((e) => e.value == index)?.key;
return getValueForKey(uniqueKey, getValue);
}
S? getValueForKey<S>(String? uniqueKey, S? Function(T) getValue) {
final model = _childrenModels[uniqueKey];
return model != null ? getValue(model) : null;
}
void dispose() => _childrenModels.values.forEach((model) => model.dispose());
void _updateActiveKeys(String uniqueKey) {
final shouldResetActiveKeys = _activeKeys == null;
_activeKeys ??= {};
_activeKeys!.add(uniqueKey);
if (shouldResetActiveKeys) {
// Add a post-frame callback to remove and dispose of unused models after
// we're done building, then reset `_activeKeys` to null so we know to do
// this again next build.
SchedulerBinding.instance.addPostFrameCallback((_) {
_childrenIndexes.removeWhere((k, _) => !_activeKeys!.contains(k));
_childrenModels.keys
.toSet()
.difference(_activeKeys!)
// Remove and dispose of unused models since they are not being used
// elsewhere and would not otherwise be disposed.
.forEach((k) => _childrenModels.remove(k)?.dispose());
_activeKeys = null;
});
}
}
}
T? _getDefaultValue<T>() {
switch (T) {
case int:
return 0 as T;
case double:
return 0.0 as T;
case String:
return '' as T;
case bool:
return false as T;
default:
return null as T;
}
}
extension TextValidationExtensions on String? Function(BuildContext, String?)? {
String? Function(String?)? asValidator(BuildContext context) =>
this != null ? (val) => this!(context, val) : null;
}

View File

@@ -0,0 +1,283 @@
/*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import 'form_field_controller.dart';
import 'package:flutter/material.dart';
class FlutterRadioButton extends StatefulWidget {
const FlutterRadioButton({
super.key,
required this.options,
required this.onChanged,
required this.controller,
required this.optionHeight,
required this.textStyle,
this.optionWidth,
this.selectedTextStyle,
this.textPadding = EdgeInsets.zero,
this.buttonPosition = RadioButtonPosition.left,
this.direction = Axis.vertical,
required this.radioButtonColor,
this.inactiveRadioButtonColor,
this.toggleable = false,
this.horizontalAlignment = WrapAlignment.start,
this.verticalAlignment = WrapCrossAlignment.start,
});
final List<String> options;
final Function(String?)? onChanged;
final FormFieldController<String> controller;
final double optionHeight;
final double? optionWidth;
final TextStyle textStyle;
final TextStyle? selectedTextStyle;
final EdgeInsetsGeometry textPadding;
final RadioButtonPosition buttonPosition;
final Axis direction;
final Color radioButtonColor;
final Color? inactiveRadioButtonColor;
final bool toggleable;
final WrapAlignment horizontalAlignment;
final WrapCrossAlignment verticalAlignment;
@override
State<FlutterRadioButton> createState() => _FlutterRadioButtonState();
}
class _FlutterRadioButtonState extends State<FlutterRadioButton> {
bool get enabled => widget.onChanged != null;
FormFieldController<String> get controller => widget.controller;
void Function()? _listener;
@override
void initState() {
super.initState();
_maybeSetOnChangedListener();
}
@override
void dispose() {
_maybeRemoveOnChangedListener();
super.dispose();
}
@override
void didUpdateWidget(FlutterRadioButton oldWidget) {
super.didUpdateWidget(oldWidget);
final oldWidgetEnabled = oldWidget.onChanged != null;
if (oldWidgetEnabled != enabled) {
_maybeRemoveOnChangedListener();
_maybeSetOnChangedListener();
}
}
void _maybeSetOnChangedListener() {
if (enabled) {
_listener = () => widget.onChanged!(controller.value);
controller.addListener(_listener!);
}
}
void _maybeRemoveOnChangedListener() {
if (_listener != null) {
controller.removeListener(_listener!);
_listener = null;
}
}
List<String> get effectiveOptions =>
widget.options.isEmpty ? ['[Option]'] : widget.options;
@override
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context)
.copyWith(unselectedWidgetColor: widget.inactiveRadioButtonColor),
child: RadioGroup<String>.builder(
direction: widget.direction,
groupValue: controller.value,
onChanged: enabled ? (value) => controller.value = value : null,
activeColor: widget.radioButtonColor,
toggleable: widget.toggleable,
textStyle: widget.textStyle,
selectedTextStyle: widget.selectedTextStyle ?? widget.textStyle,
textPadding: widget.textPadding,
optionHeight: widget.optionHeight,
optionWidth: widget.optionWidth,
horizontalAlignment: widget.horizontalAlignment,
verticalAlignment: widget.verticalAlignment,
items: effectiveOptions,
itemBuilder: (item) =>
RadioButtonBuilder(item, buttonPosition: widget.buttonPosition),
),
);
}
}
enum RadioButtonPosition {
right,
left,
}
class RadioButtonBuilder<T> {
RadioButtonBuilder(
this.description, {
this.buttonPosition = RadioButtonPosition.left,
});
final String description;
final RadioButtonPosition buttonPosition;
}
class RadioButton<T> extends StatelessWidget {
const RadioButton({
super.key,
required this.description,
required this.value,
required this.groupValue,
required this.onChanged,
required this.buttonPosition,
required this.activeColor,
required this.toggleable,
required this.textStyle,
required this.selectedTextStyle,
required this.textPadding,
this.shouldFlex = false,
});
final String description;
final T value;
final T? groupValue;
final void Function(T?)? onChanged;
final RadioButtonPosition buttonPosition;
final Color activeColor;
final bool toggleable;
final TextStyle textStyle;
final TextStyle selectedTextStyle;
final EdgeInsetsGeometry textPadding;
final bool shouldFlex;
@override
Widget build(BuildContext context) {
final selectedStyle = selectedTextStyle;
final isSelected = value == groupValue;
Widget radioButtonText = Padding(
padding: textPadding,
child: Text(
description,
style: isSelected ? selectedStyle : textStyle,
),
);
if (shouldFlex) {
radioButtonText = Flexible(child: radioButtonText);
}
return InkWell(
onTap: onChanged != null ? () => onChanged!(value) : null,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (buttonPosition == RadioButtonPosition.right) radioButtonText,
Radio<T>(
groupValue: groupValue,
onChanged: onChanged,
value: value,
activeColor: activeColor,
toggleable: toggleable,
),
if (buttonPosition == RadioButtonPosition.left) radioButtonText,
],
),
);
}
}
class RadioGroup<T> extends StatelessWidget {
const RadioGroup.builder({
super.key,
required this.groupValue,
required this.onChanged,
required this.items,
required this.itemBuilder,
required this.direction,
required this.optionHeight,
required this.horizontalAlignment,
required this.activeColor,
required this.toggleable,
required this.textStyle,
required this.selectedTextStyle,
required this.textPadding,
this.optionWidth,
this.verticalAlignment = WrapCrossAlignment.center,
});
final T? groupValue;
final List<T> items;
final RadioButtonBuilder Function(T value) itemBuilder;
final void Function(T?)? onChanged;
final Axis direction;
final double optionHeight;
final double? optionWidth;
final WrapAlignment horizontalAlignment;
final WrapCrossAlignment verticalAlignment;
final Color activeColor;
final bool toggleable;
final TextStyle textStyle;
final TextStyle selectedTextStyle;
final EdgeInsetsGeometry textPadding;
List<Widget> get _group => items.map(
(item) {
final radioButtonBuilder = itemBuilder(item);
return SizedBox(
height: optionHeight,
width: optionWidth,
child: RadioButton(
description: radioButtonBuilder.description,
value: item,
groupValue: groupValue,
onChanged: onChanged,
buttonPosition: radioButtonBuilder.buttonPosition,
activeColor: activeColor,
toggleable: toggleable,
textStyle: textStyle,
selectedTextStyle: selectedTextStyle,
textPadding: textPadding,
shouldFlex: optionWidth != null,
),
);
},
).toList();
@override
Widget build(BuildContext context) => direction == Axis.horizontal
? Wrap(
direction: direction,
alignment: horizontalAlignment,
children: _group,
)
: Wrap(
direction: direction,
crossAxisAlignment: verticalAlignment,
children: _group,
);
}

View File

@@ -0,0 +1,350 @@
// ignore_for_file: overridden_fields, annotate_overrides
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.dart';
const kThemeModeKey = '__theme_mode__';
SharedPreferences? _prefs;
abstract class FlutterTheme {
static Future initialize() async =>
_prefs = await SharedPreferences.getInstance();
static ThemeMode get themeMode {
final darkMode = _prefs?.getBool(kThemeModeKey);
return darkMode == null
? ThemeMode.system
: darkMode
? ThemeMode.dark
: ThemeMode.light;
}
static void saveThemeMode(ThemeMode mode) => mode == ThemeMode.system
? _prefs?.remove(kThemeModeKey)
: _prefs?.setBool(kThemeModeKey, mode == ThemeMode.dark);
static FlutterTheme of(BuildContext context) {
return Theme.of(context).brightness == Brightness.dark
? DarkModeTheme()
: LightModeTheme();
}
@Deprecated('Use primary instead')
Color get primaryColor => primary;
@Deprecated('Use secondary instead')
Color get secondaryColor => secondary;
@Deprecated('Use tertiary instead')
Color get tertiaryColor => tertiary;
late Color primary;
late Color secondary;
late Color tertiary;
late Color alternate;
late Color primaryText;
late Color secondaryText;
late Color primaryBackground;
late Color secondaryBackground;
late Color accent1;
late Color accent2;
late Color accent3;
late Color accent4;
late Color success;
late Color warning;
late Color error;
late Color info;
@Deprecated('Use displaySmallFamily instead')
String get title1Family => displaySmallFamily;
@Deprecated('Use displaySmall instead')
TextStyle get title1 => typography.displaySmall;
@Deprecated('Use headlineMediumFamily instead')
String get title2Family => typography.headlineMediumFamily;
@Deprecated('Use headlineMedium instead')
TextStyle get title2 => typography.headlineMedium;
@Deprecated('Use headlineSmallFamily instead')
String get title3Family => typography.headlineSmallFamily;
@Deprecated('Use headlineSmall instead')
TextStyle get title3 => typography.headlineSmall;
@Deprecated('Use titleMediumFamily instead')
String get subtitle1Family => typography.titleMediumFamily;
@Deprecated('Use titleMedium instead')
TextStyle get subtitle1 => typography.titleMedium;
@Deprecated('Use titleSmallFamily instead')
String get subtitle2Family => typography.titleSmallFamily;
@Deprecated('Use titleSmall instead')
TextStyle get subtitle2 => typography.titleSmall;
@Deprecated('Use bodyMediumFamily instead')
String get bodyText1Family => typography.bodyMediumFamily;
@Deprecated('Use bodyMedium instead')
TextStyle get bodyText1 => typography.bodyMedium;
@Deprecated('Use bodySmallFamily instead')
String get bodyText2Family => typography.bodySmallFamily;
@Deprecated('Use bodySmall instead')
TextStyle get bodyText2 => typography.bodySmall;
String get displayLargeFamily => typography.displayLargeFamily;
TextStyle get displayLarge => typography.displayLarge;
String get displayMediumFamily => typography.displayMediumFamily;
TextStyle get displayMedium => typography.displayMedium;
String get displaySmallFamily => typography.displaySmallFamily;
TextStyle get displaySmall => typography.displaySmall;
String get headlineLargeFamily => typography.headlineLargeFamily;
TextStyle get headlineLarge => typography.headlineLarge;
String get headlineMediumFamily => typography.headlineMediumFamily;
TextStyle get headlineMedium => typography.headlineMedium;
String get headlineSmallFamily => typography.headlineSmallFamily;
TextStyle get headlineSmall => typography.headlineSmall;
String get titleLargeFamily => typography.titleLargeFamily;
TextStyle get titleLarge => typography.titleLarge;
String get titleMediumFamily => typography.titleMediumFamily;
TextStyle get titleMedium => typography.titleMedium;
String get titleSmallFamily => typography.titleSmallFamily;
TextStyle get titleSmall => typography.titleSmall;
String get labelLargeFamily => typography.labelLargeFamily;
TextStyle get labelLarge => typography.labelLarge;
String get labelMediumFamily => typography.labelMediumFamily;
TextStyle get labelMedium => typography.labelMedium;
String get labelSmallFamily => typography.labelSmallFamily;
TextStyle get labelSmall => typography.labelSmall;
String get bodyLargeFamily => typography.bodyLargeFamily;
TextStyle get bodyLarge => typography.bodyLarge;
String get bodyMediumFamily => typography.bodyMediumFamily;
TextStyle get bodyMedium => typography.bodyMedium;
String get bodySmallFamily => typography.bodySmallFamily;
TextStyle get bodySmall => typography.bodySmall;
Typography get typography => ThemeTypography(this);
}
class LightModeTheme extends FlutterTheme {
@Deprecated('Use primary instead')
Color get primaryColor => primary;
@Deprecated('Use secondary instead')
Color get secondaryColor => secondary;
@Deprecated('Use tertiary instead')
Color get tertiaryColor => tertiary;
late Color primary = const Color(0xFF4B39EF);
late Color secondary = const Color(0xFF39D2C0);
late Color tertiary = const Color(0xFFEE8B60);
late Color alternate = const Color(0xFFE0E3E7);
late Color primaryText = const Color(0xFF14181B);
late Color secondaryText = const Color(0xFF57636C);
late Color primaryBackground = const Color(0xFFF1F4F8);
late Color secondaryBackground = const Color(0xFFFFFFFF);
late Color accent1 = const Color(0x4C4B39EF);
late Color accent2 = const Color(0x4D39D2C0);
late Color accent3 = const Color(0x4DEE8B60);
late Color accent4 = const Color(0xCCFFFFFF);
late Color success = const Color(0xFF249689);
late Color warning = const Color(0xFFF9CF58);
late Color error = const Color(0xFFFF5963);
late Color info = const Color(0xFFFFFFFF);
}
abstract class Typography {
String get displayLargeFamily;
TextStyle get displayLarge;
String get displayMediumFamily;
TextStyle get displayMedium;
String get displaySmallFamily;
TextStyle get displaySmall;
String get headlineLargeFamily;
TextStyle get headlineLarge;
String get headlineMediumFamily;
TextStyle get headlineMedium;
String get headlineSmallFamily;
TextStyle get headlineSmall;
String get titleLargeFamily;
TextStyle get titleLarge;
String get titleMediumFamily;
TextStyle get titleMedium;
String get titleSmallFamily;
TextStyle get titleSmall;
String get labelLargeFamily;
TextStyle get labelLarge;
String get labelMediumFamily;
TextStyle get labelMedium;
String get labelSmallFamily;
TextStyle get labelSmall;
String get bodyLargeFamily;
TextStyle get bodyLarge;
String get bodyMediumFamily;
TextStyle get bodyMedium;
String get bodySmallFamily;
TextStyle get bodySmall;
}
class ThemeTypography extends Typography {
ThemeTypography(this.theme);
final FlutterTheme theme;
String get displayLargeFamily => 'Outfit';
TextStyle get displayLarge => GoogleFonts.getFont(
'Outfit',
color: theme.primaryText,
fontWeight: FontWeight.normal,
fontSize: 64.0,
);
String get displayMediumFamily => 'Outfit';
TextStyle get displayMedium => GoogleFonts.getFont(
'Outfit',
color: theme.primaryText,
fontWeight: FontWeight.normal,
fontSize: 44.0,
);
String get displaySmallFamily => 'Outfit';
TextStyle get displaySmall => GoogleFonts.getFont(
'Outfit',
color: theme.primaryText,
fontWeight: FontWeight.w600,
fontSize: 36.0,
);
String get headlineLargeFamily => 'Outfit';
TextStyle get headlineLarge => GoogleFonts.getFont(
'Outfit',
color: theme.primaryText,
fontWeight: FontWeight.w600,
fontSize: 32.0,
);
String get headlineMediumFamily => 'Outfit';
TextStyle get headlineMedium => GoogleFonts.getFont(
'Outfit',
color: theme.primaryText,
fontWeight: FontWeight.normal,
fontSize: 24.0,
);
String get headlineSmallFamily => 'Outfit';
TextStyle get headlineSmall => GoogleFonts.getFont(
'Outfit',
color: theme.primaryText,
fontWeight: FontWeight.w500,
fontSize: 24.0,
);
String get titleLargeFamily => 'Outfit';
TextStyle get titleLarge => GoogleFonts.getFont(
'Outfit',
color: theme.primaryText,
fontWeight: FontWeight.w500,
fontSize: 22.0,
);
String get titleMediumFamily => 'Readex Pro';
TextStyle get titleMedium => GoogleFonts.getFont(
'Readex Pro',
color: theme.info,
fontWeight: FontWeight.normal,
fontSize: 18.0,
);
String get titleSmallFamily => 'Readex Pro';
TextStyle get titleSmall => GoogleFonts.getFont(
'Readex Pro',
color: theme.info,
fontWeight: FontWeight.w500,
fontSize: 16.0,
);
String get labelLargeFamily => 'Readex Pro';
TextStyle get labelLarge => GoogleFonts.getFont(
'Readex Pro',
color: theme.secondaryText,
fontWeight: FontWeight.normal,
fontSize: 16.0,
);
String get labelMediumFamily => 'Readex Pro';
TextStyle get labelMedium => GoogleFonts.getFont(
'Readex Pro',
color: theme.secondaryText,
fontWeight: FontWeight.normal,
fontSize: 14.0,
);
String get labelSmallFamily => 'Readex Pro';
TextStyle get labelSmall => GoogleFonts.getFont(
'Readex Pro',
color: theme.secondaryText,
fontWeight: FontWeight.normal,
fontSize: 12.0,
);
String get bodyLargeFamily => 'Readex Pro';
TextStyle get bodyLarge => GoogleFonts.getFont(
'Readex Pro',
color: theme.primaryText,
fontWeight: FontWeight.normal,
fontSize: 16.0,
);
String get bodyMediumFamily => 'Readex Pro';
TextStyle get bodyMedium => GoogleFonts.getFont(
'Readex Pro',
color: theme.primaryText,
fontWeight: FontWeight.normal,
fontSize: 14.0,
);
String get bodySmallFamily => 'Readex Pro';
TextStyle get bodySmall => GoogleFonts.getFont(
'Readex Pro',
color: theme.primaryText,
fontWeight: FontWeight.normal,
fontSize: 12.0,
);
}
class DarkModeTheme extends FlutterTheme {
@Deprecated('Use primary instead')
Color get primaryColor => primary;
@Deprecated('Use secondary instead')
Color get secondaryColor => secondary;
@Deprecated('Use tertiary instead')
Color get tertiaryColor => tertiary;
late Color primary = const Color(0xFF4B39EF);
late Color secondary = const Color(0xFF39D2C0);
late Color tertiary = const Color(0xFFEE8B60);
late Color alternate = const Color(0xFF262D34);
late Color primaryText = const Color(0xFFFFFFFF);
late Color secondaryText = const Color(0xFF95A1AC);
late Color primaryBackground = const Color(0xFF1D2428);
late Color secondaryBackground = const Color(0xFF14181B);
late Color accent1 = const Color(0x4C4B39EF);
late Color accent2 = const Color(0x4D39D2C0);
late Color accent3 = const Color(0x4DEE8B60);
late Color accent4 = const Color(0xB2262D34);
late Color success = const Color(0xFF249689);
late Color warning = const Color(0xFFF9CF58);
late Color error = const Color(0xFFFF5963);
late Color info = const Color(0xFFFFFFFF);
}
extension TextStyleHelper on TextStyle {
TextStyle override({
String? fontFamily,
Color? color,
double? fontSize,
FontWeight? fontWeight,
double? letterSpacing,
FontStyle? fontStyle,
bool useGoogleFonts = true,
TextDecoration? decoration,
double? lineHeight,
}) =>
useGoogleFonts
? GoogleFonts.getFont(
fontFamily!,
color: color ?? this.color,
fontSize: fontSize ?? this.fontSize,
letterSpacing: letterSpacing ?? this.letterSpacing,
fontWeight: fontWeight ?? this.fontWeight,
fontStyle: fontStyle ?? this.fontStyle,
decoration: decoration,
height: lineHeight,
)
: copyWith(
fontFamily: fontFamily,
color: color,
fontSize: fontSize,
letterSpacing: letterSpacing,
fontWeight: fontWeight,
fontStyle: fontStyle,
decoration: decoration,
height: lineHeight,
);
}

View File

@@ -0,0 +1,481 @@
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:from_css_color/from_css_color.dart';
import 'package:intl/intl.dart';
import 'package:json_path/json_path.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:url_launcher/url_launcher.dart';
import '../main.dart';
import 'lat_lng.dart';
export 'keep_alive_wrapper.dart';
export 'lat_lng.dart';
export 'place.dart';
export 'uploaded_file.dart';
export '../app_state.dart';
export 'flutter_model.dart';
export 'dart:math' show min, max;
export 'dart:typed_data' show Uint8List;
export 'dart:convert' show jsonEncode, jsonDecode;
export 'package:intl/intl.dart';
export 'package:page_transition/page_transition.dart';
export 'internationalization.dart' show FFLocalizations;
export 'nav/nav.dart';
T valueOrDefault<T>(T? value, T defaultValue) =>
(value is String && value.isEmpty) || value == null ? defaultValue : value;
void _setTimeagoLocales() {
timeago.setLocaleMessages('en', timeago.EnMessages());
timeago.setLocaleMessages('en_short', timeago.EnShortMessages());
timeago.setLocaleMessages('zh_Hant', timeago.ZhMessages());
}
String dateTimeFormat(String format, DateTime? dateTime, {String? locale}) {
if (dateTime == null) {
return '';
}
if (format == 'relative') {
_setTimeagoLocales();
return timeago.format(dateTime, locale: locale, allowFromNow: true);
}
return DateFormat(format, locale).format(dateTime);
}
Theme wrapInMaterialDatePickerTheme(
BuildContext context,
Widget child, {
required Color headerBackgroundColor,
required Color headerForegroundColor,
required TextStyle headerTextStyle,
required Color pickerBackgroundColor,
required Color pickerForegroundColor,
required Color selectedDateTimeBackgroundColor,
required Color selectedDateTimeForegroundColor,
required Color actionButtonForegroundColor,
required double iconSize,
}) {
final baseTheme = Theme.of(context);
final dateTimeMaterialStateForegroundColor =
MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.disabled)) {
return pickerForegroundColor.withOpacity(0.60);
}
if (states.contains(MaterialState.selected)) {
return selectedDateTimeForegroundColor;
}
if (states.isEmpty) {
return pickerForegroundColor;
}
return null;
});
final dateTimeMaterialStateBackgroundColor =
MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
return selectedDateTimeBackgroundColor;
}
return null;
});
return Theme(
data: baseTheme.copyWith(
colorScheme: baseTheme.colorScheme.copyWith(
onSurface: pickerForegroundColor,
),
disabledColor: pickerForegroundColor.withOpacity(0.3),
textTheme: baseTheme.textTheme.copyWith(
headlineSmall: headerTextStyle,
headlineMedium: headerTextStyle,
),
iconTheme: baseTheme.iconTheme.copyWith(
size: iconSize,
),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
foregroundColor: MaterialStatePropertyAll(
actionButtonForegroundColor,
),
overlayColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered)) {
return actionButtonForegroundColor.withOpacity(0.04);
}
if (states.contains(MaterialState.focused) ||
states.contains(MaterialState.pressed)) {
return actionButtonForegroundColor.withOpacity(0.12);
}
return null;
})),
),
datePickerTheme: DatePickerThemeData(
backgroundColor: pickerBackgroundColor,
headerBackgroundColor: headerBackgroundColor,
headerForegroundColor: headerForegroundColor,
weekdayStyle: baseTheme.textTheme.labelMedium!.copyWith(
color: pickerForegroundColor,
),
dayBackgroundColor: dateTimeMaterialStateBackgroundColor,
todayBackgroundColor: dateTimeMaterialStateBackgroundColor,
yearBackgroundColor: dateTimeMaterialStateBackgroundColor,
dayForegroundColor: dateTimeMaterialStateForegroundColor,
todayForegroundColor: dateTimeMaterialStateForegroundColor,
yearForegroundColor: dateTimeMaterialStateForegroundColor,
),
),
child: child,
);
}
Theme wrapInMaterialTimePickerTheme(
BuildContext context,
Widget child, {
required Color headerBackgroundColor,
required Color headerForegroundColor,
required TextStyle headerTextStyle,
required Color pickerBackgroundColor,
required Color pickerForegroundColor,
required Color selectedDateTimeBackgroundColor,
required Color selectedDateTimeForegroundColor,
required Color actionButtonForegroundColor,
required double iconSize,
}) {
final baseTheme = Theme.of(context);
return Theme(
data: baseTheme.copyWith(
iconTheme: baseTheme.iconTheme.copyWith(
size: iconSize,
),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
foregroundColor: MaterialStatePropertyAll(
actionButtonForegroundColor,
),
overlayColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered)) {
return actionButtonForegroundColor.withOpacity(0.04);
}
if (states.contains(MaterialState.focused) ||
states.contains(MaterialState.pressed)) {
return actionButtonForegroundColor.withOpacity(0.12);
}
return null;
})),
),
timePickerTheme: baseTheme.timePickerTheme.copyWith(
backgroundColor: pickerBackgroundColor,
hourMinuteTextColor: pickerForegroundColor,
dialHandColor: selectedDateTimeBackgroundColor,
dialTextColor: MaterialStateColor.resolveWith((states) =>
states.contains(MaterialState.selected)
? selectedDateTimeForegroundColor
: pickerForegroundColor),
dayPeriodBorderSide: BorderSide(
color: pickerForegroundColor,
),
dayPeriodTextColor: MaterialStateColor.resolveWith((states) =>
states.contains(MaterialState.selected)
? selectedDateTimeForegroundColor
: pickerForegroundColor),
dayPeriodColor: MaterialStateColor.resolveWith((states) =>
states.contains(MaterialState.selected)
? selectedDateTimeBackgroundColor
: Colors.transparent),
entryModeIconColor: pickerForegroundColor,
),
),
child: child,
);
}
Future launchURL(String url) async {
var uri = Uri.parse(url).toString();
try {
await launch(uri);
} catch (e) {
throw 'Could not launch $uri: $e';
}
}
Color colorFromCssString(String color, {Color? defaultColor}) {
try {
return fromCssColor(color);
} catch (_) {}
return defaultColor ?? Colors.black;
}
enum FormatType {
decimal,
percent,
scientific,
compact,
compactLong,
custom,
}
enum DecimalType {
automatic,
periodDecimal,
commaDecimal,
}
String formatNumber(
num? value, {
required FormatType formatType,
DecimalType? decimalType,
String? currency,
bool toLowerCase = false,
String? format,
String? locale,
}) {
if (value == null) {
return '';
}
var formattedValue = '';
switch (formatType) {
case FormatType.decimal:
switch (decimalType!) {
case DecimalType.automatic:
formattedValue = NumberFormat.decimalPattern().format(value);
break;
case DecimalType.periodDecimal:
formattedValue = NumberFormat.decimalPattern('en_US').format(value);
break;
case DecimalType.commaDecimal:
formattedValue = NumberFormat.decimalPattern('es_PA').format(value);
break;
}
break;
case FormatType.percent:
formattedValue = NumberFormat.percentPattern().format(value);
break;
case FormatType.scientific:
formattedValue = NumberFormat.scientificPattern().format(value);
if (toLowerCase) {
formattedValue = formattedValue.toLowerCase();
}
break;
case FormatType.compact:
formattedValue = NumberFormat.compact().format(value);
break;
case FormatType.compactLong:
formattedValue = NumberFormat.compactLong().format(value);
break;
case FormatType.custom:
final hasLocale = locale != null && locale.isNotEmpty;
formattedValue =
NumberFormat(format, hasLocale ? locale : null).format(value);
}
if (formattedValue.isEmpty) {
return value.toString();
}
if (currency != null) {
final currencySymbol = currency.isNotEmpty
? currency
: NumberFormat.simpleCurrency().format(0.0).substring(0, 1);
formattedValue = '$currencySymbol$formattedValue';
}
return formattedValue;
}
DateTime get getCurrentTimestamp => DateTime.now();
DateTime dateTimeFromSecondsSinceEpoch(int seconds) {
return DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
}
extension DateTimeConversionExtension on DateTime {
int get secondsSinceEpoch => (millisecondsSinceEpoch / 1000).round();
}
extension DateTimeComparisonOperators on DateTime {
bool operator <(DateTime other) => isBefore(other);
bool operator >(DateTime other) => isAfter(other);
bool operator <=(DateTime other) => this < other || isAtSameMomentAs(other);
bool operator >=(DateTime other) => this > other || isAtSameMomentAs(other);
}
T? castToType<T>(dynamic value) {
if (value == null) {
return null;
}
switch (T) {
case double:
// Doubles may be stored as ints in some cases.
return value.toDouble() as T;
case int:
// Likewise, ints may be stored as doubles. If this is the case
// (i.e. no decimal value), return the value as an int.
if (value is num && value.toInt() == value) {
return value.toInt() as T;
}
break;
default:
break;
}
return value as T;
}
dynamic getJsonField(
dynamic response,
String jsonPath, [
bool isForList = false,
]) {
final field = JsonPath(jsonPath).read(response);
if (field.isEmpty) {
return null;
}
if (field.length > 1) {
return field.map((f) => f.value).toList();
}
final value = field.first.value;
if (isForList) {
return value is! Iterable
? [value]
: (value is List ? value : value.toList());
}
return value;
}
Rect? getWidgetBoundingBox(BuildContext context) {
try {
final renderBox = context.findRenderObject() as RenderBox?;
return renderBox!.localToGlobal(Offset.zero) & renderBox.size;
} catch (_) {
return null;
}
}
bool get isAndroid => !kIsWeb && Platform.isAndroid;
bool get isiOS => !kIsWeb && Platform.isIOS;
bool get isWeb => kIsWeb;
const kBreakpointSmall = 479.0;
const kBreakpointMedium = 767.0;
const kBreakpointLarge = 991.0;
bool isMobileWidth(BuildContext context) =>
MediaQuery.sizeOf(context).width < kBreakpointSmall;
bool responsiveVisibility({
required BuildContext context,
bool phone = true,
bool tablet = true,
bool tabletLandscape = true,
bool desktop = true,
}) {
final width = MediaQuery.sizeOf(context).width;
if (width < kBreakpointSmall) {
return phone;
} else if (width < kBreakpointMedium) {
return tablet;
} else if (width < kBreakpointLarge) {
return tabletLandscape;
} else {
return desktop;
}
}
const kTextValidatorUsernameRegex = r'^[a-zA-Z][a-zA-Z0-9_-]{2,16}$';
// https://stackoverflow.com/a/201378
const kTextValidatorEmailRegex =
"^(?:[a-z0-9!#\$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#\$%&\'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])\$";
const kTextValidatorWebsiteRegex =
r'(https?:\/\/)?(www\.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,10}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)|(https?:\/\/)?(www\.)?(?!ww)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,10}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)';
extension FFTextEditingControllerExt on TextEditingController? {
String get text => this == null ? '' : this!.text;
set text(String newText) => this?.text = newText;
}
extension IterableExt<T> on Iterable<T> {
List<T> sortedList<S extends Comparable>([S Function(T)? keyOf]) => toList()
..sort(keyOf == null ? null : ((a, b) => keyOf(a).compareTo(keyOf(b))));
List<S> mapIndexed<S>(S Function(int, T) func) => toList()
.asMap()
.map((index, value) => MapEntry(index, func(index, value)))
.values
.toList();
}
void setAppLanguage(BuildContext context, String language) =>
MyApp.of(context).setLocale(language);
void setDarkModeSetting(BuildContext context, ThemeMode themeMode) =>
MyApp.of(context).setThemeMode(themeMode);
void showSnackbar(
BuildContext context,
String message, {
bool loading = false,
int duration = 4,
}) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
if (loading)
Padding(
padding: EdgeInsetsDirectional.only(end: 10.0),
child: Container(
height: 20,
width: 20,
child: const CircularProgressIndicator(
color: Colors.white,
),
),
),
Text(message),
],
),
duration: Duration(seconds: duration),
),
);
}
extension FFStringExt on String {
String maybeHandleOverflow({int? maxChars, String replacement = ''}) =>
maxChars != null && length > maxChars
? replaceRange(maxChars, null, replacement)
: this;
}
extension ListFilterExt<T> on Iterable<T?> {
List<T> get withoutNulls => where((s) => s != null).map((e) => e!).toList();
}
extension ListDivideExt<T extends Widget> on Iterable<T> {
Iterable<MapEntry<int, Widget>> get enumerate => toList().asMap().entries;
List<Widget> divide(Widget t) => isEmpty
? []
: (enumerate.map((e) => [e.value, t]).expand((i) => i).toList()
..removeLast());
List<Widget> around(Widget t) => addToStart(t).addToEnd(t);
List<Widget> addToStart(Widget t) =>
enumerate.map((e) => e.value).toList()..insert(0, t);
List<Widget> addToEnd(Widget t) =>
enumerate.map((e) => e.value).toList()..add(t);
List<Padding> paddingTopEach(double val) =>
map((w) => Padding(padding: EdgeInsets.only(top: val), child: w))
.toList();
}
extension StatefulWidgetExtensions on State<StatefulWidget> {
/// Check if the widget exist before safely setting state.
void safeSetState(VoidCallback fn) {
if (mounted) {
// ignore: invalid_use_of_protected_member
setState(fn);
}
}
}

View File

@@ -0,0 +1,257 @@
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
class FFButtonOptions {
const FFButtonOptions({
this.textStyle,
this.elevation,
this.height,
this.width,
this.padding,
this.color,
this.disabledColor,
this.disabledTextColor,
this.splashColor,
this.iconSize,
this.iconColor,
this.iconPadding,
this.borderRadius,
this.borderSide,
this.hoverColor,
this.hoverBorderSide,
this.hoverTextColor,
this.hoverElevation,
this.maxLines,
});
final TextStyle? textStyle;
final double? elevation;
final double? height;
final double? width;
final EdgeInsetsGeometry? padding;
final Color? color;
final Color? disabledColor;
final Color? disabledTextColor;
final int? maxLines;
final Color? splashColor;
final double? iconSize;
final Color? iconColor;
final EdgeInsetsGeometry? iconPadding;
final BorderRadius? borderRadius;
final BorderSide? borderSide;
final Color? hoverColor;
final BorderSide? hoverBorderSide;
final Color? hoverTextColor;
final double? hoverElevation;
}
class FFButtonWidget extends StatefulWidget {
const FFButtonWidget({
Key? key,
required this.text,
required this.onPressed,
this.icon,
this.iconData,
required this.options,
this.showLoadingIndicator = true,
}) : super(key: key);
final String text;
final Widget? icon;
final IconData? iconData;
final Function()? onPressed;
final FFButtonOptions options;
final bool showLoadingIndicator;
@override
State<FFButtonWidget> createState() => _FFButtonWidgetState();
}
class _FFButtonWidgetState extends State<FFButtonWidget> {
bool loading = false;
int get maxLines => widget.options.maxLines ?? 1;
@override
Widget build(BuildContext context) {
Widget textWidget = loading
? SizedBox(
width: widget.options.width == null
? _getTextWidth(widget.text, widget.options.textStyle, maxLines)
: null,
child: Center(
child: SizedBox(
width: 23,
height: 23,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
widget.options.textStyle!.color ?? Colors.white,
),
),
),
),
)
: AutoSizeText(
widget.text,
style: widget.options.textStyle?.withoutColor(),
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
);
final onPressed = widget.onPressed != null
? (widget.showLoadingIndicator
? () async {
if (loading) {
return;
}
setState(() => loading = true);
try {
await widget.onPressed!();
} finally {
if (mounted) {
setState(() => loading = false);
}
}
}
: () => widget.onPressed!())
: null;
ButtonStyle style = ButtonStyle(
shape: MaterialStateProperty.resolveWith<OutlinedBorder>(
(states) {
if (states.contains(MaterialState.hovered) &&
widget.options.hoverBorderSide != null) {
return RoundedRectangleBorder(
borderRadius:
widget.options.borderRadius ?? BorderRadius.circular(8),
side: widget.options.hoverBorderSide!,
);
}
return RoundedRectangleBorder(
borderRadius:
widget.options.borderRadius ?? BorderRadius.circular(8),
side: widget.options.borderSide ?? BorderSide.none,
);
},
),
foregroundColor: MaterialStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(MaterialState.disabled) &&
widget.options.disabledTextColor != null) {
return widget.options.disabledTextColor;
}
if (states.contains(MaterialState.hovered) &&
widget.options.hoverTextColor != null) {
return widget.options.hoverTextColor;
}
return widget.options.textStyle?.color;
},
),
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(MaterialState.disabled) &&
widget.options.disabledColor != null) {
return widget.options.disabledColor;
}
if (states.contains(MaterialState.hovered) &&
widget.options.hoverColor != null) {
return widget.options.hoverColor;
}
return widget.options.color;
},
),
overlayColor: MaterialStateProperty.resolveWith<Color?>((states) {
if (states.contains(MaterialState.pressed)) {
return widget.options.splashColor;
}
return widget.options.hoverColor == null ? null : Colors.transparent;
}),
padding: MaterialStateProperty.all(widget.options.padding ??
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0)),
elevation: MaterialStateProperty.resolveWith<double?>(
(states) {
if (states.contains(MaterialState.hovered) &&
widget.options.hoverElevation != null) {
return widget.options.hoverElevation!;
}
return widget.options.elevation;
},
),
);
if ((widget.icon != null || widget.iconData != null) && !loading) {
return Container(
height: widget.options.height,
width: widget.options.width,
child: ElevatedButton.icon(
icon: Padding(
padding: widget.options.iconPadding ?? EdgeInsets.zero,
child: widget.icon ??
FaIcon(
widget.iconData,
size: widget.options.iconSize,
color: widget.options.iconColor ??
widget.options.textStyle!.color,
),
),
label: textWidget,
onPressed: onPressed,
style: style,
),
);
}
return Container(
height: widget.options.height,
width: widget.options.width,
child: ElevatedButton(
onPressed: onPressed,
style: style,
child: textWidget,
),
);
}
}
extension _WithoutColorExtension on TextStyle {
TextStyle withoutColor() => TextStyle(
inherit: inherit,
color: null,
backgroundColor: backgroundColor,
fontSize: fontSize,
fontWeight: fontWeight,
fontStyle: fontStyle,
letterSpacing: letterSpacing,
wordSpacing: wordSpacing,
textBaseline: textBaseline,
height: height,
leadingDistribution: leadingDistribution,
locale: locale,
foreground: foreground,
background: background,
shadows: shadows,
fontFeatures: fontFeatures,
decoration: decoration,
decorationColor: decorationColor,
decorationStyle: decorationStyle,
decorationThickness: decorationThickness,
debugLabel: debugLabel,
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
// The _package field is private so unfortunately we can't set it here,
// but it's almost always unset anyway.
// package: _package,
overflow: overflow,
);
}
// Slightly hacky method of getting the layout width of the provided text.
double _getTextWidth(String text, TextStyle? style, int maxLines) =>
(TextPainter(
text: TextSpan(text: text, style: style),
textDirection: TextDirection.ltr,
maxLines: maxLines,
)..layout())
.size
.width;

View File

@@ -0,0 +1,9 @@
import 'package:flutter/foundation.dart';
class FormFieldController<T> extends ValueNotifier<T?> {
FormFieldController(this.initialValue) : super(initialValue);
final T? initialValue;
void reset() => value = initialValue;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class KeepAliveWidgetWrapper extends StatefulWidget {
const KeepAliveWidgetWrapper({
Key? key,
required this.builder,
}) : super(key: key);
final WidgetBuilder builder;
@override
State<KeepAliveWidgetWrapper> createState() => _KeepAliveWidgetWrapperState();
}
class _KeepAliveWidgetWrapperState extends State<KeepAliveWidgetWrapper>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return widget.builder(context);
}
}

View File

@@ -0,0 +1,19 @@
class LatLng {
const LatLng(this.latitude, this.longitude);
final double latitude;
final double longitude;
@override
String toString() => 'LatLng(lat: $latitude, lng: $longitude)';
String serialize() => '$latitude,$longitude';
@override
int get hashCode => latitude.hashCode + longitude.hashCode;
@override
bool operator ==(other) =>
other is LatLng &&
latitude == other.latitude &&
longitude == other.longitude;
}

686
lib/flutterlib/nav/nav.dart Normal file
View File

@@ -0,0 +1,686 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:page_transition/page_transition.dart';
import 'package:provider/provider.dart';
import '/backend/schema/enums/enums.dart';
import '/auth/custom_auth/custom_auth_user_provider.dart';
import '/index.dart';
import '/main.dart';
import '/flutterlib/flutter_theme.dart';
import '/flutterlib/lat_lng.dart';
import '/flutterlib/place.dart';
import '/flutterlib/flutter_util.dart';
import 'serialization_util.dart';
export 'package:go_router/go_router.dart';
export 'serialization_util.dart';
const kTransitionInfoKey = '__transition_info__';
class AppStateNotifier extends ChangeNotifier {
AppStateNotifier._();
static AppStateNotifier? _instance;
static AppStateNotifier get instance => _instance ??= AppStateNotifier._();
NumstationAuthUser? initialUser;
NumstationAuthUser? user;
bool showSplashImage = true;
String? _redirectLocation;
/// Determines whether the app will refresh and build again when a sign
/// in or sign out happens. This is useful when the app is launched or
/// on an unexpected logout. However, this must be turned off when we
/// intend to sign in/out and then navigate or perform any actions after.
/// Otherwise, this will trigger a refresh and interrupt the action(s).
bool notifyOnAuthChange = true;
bool get loading => user == null || showSplashImage;
bool get loggedIn => user?.loggedIn ?? false;
bool get initiallyLoggedIn => initialUser?.loggedIn ?? false;
bool get shouldRedirect => loggedIn && _redirectLocation != null;
String getRedirectLocation() => _redirectLocation!;
bool hasRedirect() => _redirectLocation != null;
void setRedirectLocationIfUnset(String loc) => _redirectLocation ??= loc;
void clearRedirectLocation() => _redirectLocation = null;
/// Mark as not needing to notify on a sign in / out when we intend
/// to perform subsequent actions (such as navigation) afterwards.
void updateNotifyOnAuthChange(bool notify) => notifyOnAuthChange = notify;
void update(NumstationAuthUser newUser) {
final shouldUpdate =
user?.uid == null || newUser.uid == null || user?.uid != newUser.uid;
initialUser ??= newUser;
user = newUser;
// Refresh the app on auth change unless explicitly marked otherwise.
// No need to update unless the user has changed.
if (notifyOnAuthChange && shouldUpdate) {
notifyListeners();
}
// Once again mark the notifier as needing to update on auth change
// (in order to catch sign in / out events).
updateNotifyOnAuthChange(true);
}
void stopShowingSplashImage() {
showSplashImage = false;
notifyListeners();
}
}
GoRouter createRouter(AppStateNotifier appStateNotifier) => GoRouter(
initialLocation: '/',
debugLogDiagnostics: true,
refreshListenable: appStateNotifier,
errorBuilder: (context, state) =>
appStateNotifier.loggedIn ? MenuWidget() : LoginWidget(),
routes: [
FFRoute(
name: '_initialize',
path: '/',
builder: (context, _) =>
appStateNotifier.loggedIn ? MenuWidget() : LoginWidget(),
),
FFRoute(
name: 'login',
path: '/login',
builder: (context, params) => LoginWidget(),
),
FFRoute(
name: 'signup',
path: '/signup',
builder: (context, params) => SignupWidget(),
),
FFRoute(
name: 'login1',
path: '/login1',
builder: (context, params) => Login1Widget(),
),
FFRoute(
name: 'login2',
path: '/login2',
builder: (context, params) => Login2Widget(),
),
FFRoute(
name: 'login3',
path: '/login3',
builder: (context, params) => Login3Widget(),
),
FFRoute(
name: 'login4',
path: '/login4',
builder: (context, params) => Login4Widget(),
),
FFRoute(
name: 'login5',
path: '/login5',
builder: (context, params) => Login5Widget(),
),
FFRoute(
name: 'tnc',
path: '/tnc',
builder: (context, params) => TncWidget(),
),
FFRoute(
name: 'policy',
path: '/policy',
builder: (context, params) => PolicyWidget(),
),
FFRoute(
name: 'menu',
path: '/menu',
builder: (context, params) => MenuWidget(),
),
FFRoute(
name: 'dashboard',
path: '/dashboard',
builder: (context, params) => DashboardWidget(),
),
FFRoute(
name: 'test',
path: '/test',
builder: (context, params) => TestWidget(),
),
FFRoute(
name: 'setting',
path: '/setting',
builder: (context, params) => SettingWidget(),
),
FFRoute(
name: 'user',
path: '/user',
builder: (context, params) => UserWidget(),
),
FFRoute(
name: 'bk1',
path: '/bk1',
builder: (context, params) => Bk1Widget(),
),
FFRoute(
name: 'enquiry',
path: '/enquiry',
builder: (context, params) => EnquiryWidget(),
),
FFRoute(
name: 'myenquiry',
path: '/myenquiry',
builder: (context, params) => MyenquiryWidget(),
),
FFRoute(
name: 'myenquiry1',
path: '/myenquiry1',
builder: (context, params) => Myenquiry1Widget(),
),
FFRoute(
name: 'language',
path: '/language',
builder: (context, params) => LanguageWidget(),
),
FFRoute(
name: 'account1',
path: '/account1',
builder: (context, params) => Account1Widget(),
),
FFRoute(
name: 'chatbox',
path: '/chatbox',
builder: (context, params) => ChatboxWidget(),
),
FFRoute(
name: 'compsec',
path: '/compsec',
builder: (context, params) => CompsecWidget(),
),
FFRoute(
name: 'compsec1',
path: '/compsec1',
builder: (context, params) => Compsec1Widget(),
),
FFRoute(
name: 'compsec2',
path: '/compsec2',
builder: (context, params) => Compsec2Widget(),
),
FFRoute(
name: 'compsec3',
path: '/compsec3',
builder: (context, params) => Compsec3Widget(),
),
FFRoute(
name: 'compsec4',
path: '/compsec4',
builder: (context, params) => Compsec4Widget(),
),
FFRoute(
name: 'compsec5',
path: '/compsec5',
builder: (context, params) => Compsec5Widget(),
),
FFRoute(
name: 'bk2',
path: '/bk2',
builder: (context, params) => Bk2Widget(),
),
FFRoute(
name: 'bk3',
path: '/bk3',
builder: (context, params) => Bk3Widget(),
),
FFRoute(
name: 'bk4',
path: '/bk4',
builder: (context, params) => Bk4Widget(),
),
FFRoute(
name: 'bk4_1',
path: '/bk41',
builder: (context, params) => Bk41Widget(),
),
FFRoute(
name: 'bk4_2',
path: '/bk42',
builder: (context, params) => Bk42Widget(),
),
FFRoute(
name: 'search',
path: '/search',
builder: (context, params) => SearchWidget(),
),
FFRoute(
name: 'bk_checkout',
path: '/bkCheckout',
builder: (context, params) => BkCheckoutWidget(),
),
FFRoute(
name: 'bk_checkout1',
path: '/bkCheckout1',
builder: (context, params) => BkCheckout1Widget(),
),
FFRoute(
name: 'compsec_checkout',
path: '/compsecCheckout',
builder: (context, params) => CompsecCheckoutWidget(),
),
FFRoute(
name: 'compsec_checkout1',
path: '/compsecCheckout1',
builder: (context, params) => CompsecCheckout1Widget(),
),
FFRoute(
name: 'compsec_checkout2',
path: '/compsecCheckout2',
builder: (context, params) => CompsecCheckout2Widget(),
),
FFRoute(
name: 'compsecEnq',
path: '/compsecEnq',
builder: (context, params) => CompsecEnqWidget(),
),
FFRoute(
name: 'compsecEnq1',
path: '/compsecEnq1',
builder: (context, params) => CompsecEnq1Widget(),
),
FFRoute(
name: 'compsec_changename',
path: '/compsecChangename',
builder: (context, params) => CompsecChangenameWidget(),
),
FFRoute(
name: 'compsec_changeaddress',
path: '/compsecChangeaddress',
builder: (context, params) => CompsecChangeaddressWidget(),
),
FFRoute(
name: 'compsec_changedirector',
path: '/compsecChangedirector',
builder: (context, params) => CompsecChangedirectorWidget(),
),
FFRoute(
name: 'compsec_changesecretary',
path: '/compsecChangesecretary',
builder: (context, params) => CompsecChangesecretaryWidget(),
),
FFRoute(
name: 'compsec_changesecrORdir',
path: '/compsecChangesecrORdir',
builder: (context, params) => CompsecChangesecrORdirWidget(),
),
FFRoute(
name: 'compsec_resignation',
path: '/compsecResignation',
builder: (context, params) => CompsecResignationWidget(),
),
FFRoute(
name: 'compsec_transferofShares',
path: '/compsecTransferofShares',
builder: (context, params) => CompsecTransferofSharesWidget(),
),
FFRoute(
name: 'compsec_otherRequest',
path: '/compsecOtherRequest',
builder: (context, params) => CompsecOtherRequestWidget(),
),
FFRoute(
name: 'account',
path: '/account',
builder: (context, params) => AccountWidget(),
),
FFRoute(
name: 'notification',
path: '/notification',
builder: (context, params) => NotificationWidget(),
),
FFRoute(
name: 'compsecQueue',
path: '/compsecQueue',
builder: (context, params) => CompsecQueueWidget(),
),
FFRoute(
name: 'compsecQueue1_1',
path: '/compsecQueue11',
builder: (context, params) => CompsecQueue11Widget(),
),
FFRoute(
name: 'testing',
path: '/testing',
builder: (context, params) => TestingWidget(),
),
FFRoute(
name: 'newuser',
path: '/newuser',
builder: (context, params) => NewuserWidget(
checkPermission23:
params.getParam('checkPermission23', ParamType.bool),
checkPremission22:
params.getParam('checkPremission22', ParamType.bool),
),
),
FFRoute(
name: 'userDetail',
path: '/userDetail',
builder: (context, params) => UserDetailWidget(
firstName: params.getParam('firstName', ParamType.String),
lastName: params.getParam('lastName', ParamType.String),
phone: params.getParam('phone', ParamType.int),
email: params.getParam('email', ParamType.String),
status: params.getParam('status', ParamType.String),
),
),
FFRoute(
name: 'compsecDigitalTrans_list',
path: '/compsecDigitalTransList',
builder: (context, params) => CompsecDigitalTransListWidget(),
),
FFRoute(
name: 'compsecServicedraft',
path: '/compsecServicedraft',
builder: (context, params) => CompsecServicedraftWidget(),
),
FFRoute(
name: 'setting1',
path: '/setting1',
builder: (context, params) => Setting1Widget(),
),
FFRoute(
name: 'editUser',
path: '/editUser',
builder: (context, params) => EditUserWidget(
firstName: params.getParam('firstName', ParamType.String),
lastName: params.getParam('lastName', ParamType.String),
phone: params.getParam('phone', ParamType.int),
email: params.getParam('email', ParamType.String),
userID: params.getParam('userID', ParamType.int),
checkPermission23:
params.getParam('checkPermission23', ParamType.bool),
checkPremission22:
params.getParam('checkPremission22', ParamType.bool),
roleID: params.getParam('roleID', ParamType.String),
),
),
FFRoute(
name: 'bk_view',
path: '/bkView',
builder: (context, params) => BkViewWidget(
fileUrl: params.getParam('fileUrl', ParamType.JSON),
),
),
FFRoute(
name: 'search1',
path: '/search1',
builder: (context, params) => Search1Widget(
search: params.getParam('search', ParamType.String),
status: params.getParam('status', ParamType.String),
category: params.getParam('category', ParamType.String),
fixDate: params.getParam('fixDate', ParamType.String),
dateFrom: params.getParam('dateFrom', ParamType.String),
dateTo: params.getParam('dateTo', ParamType.String),
),
),
FFRoute(
name: 'search_page',
path: '/searchPage',
builder: (context, params) => SearchPageWidget(),
)
].map((r) => r.toRoute(appStateNotifier)).toList(),
);
extension NavParamExtensions on Map<String, String?> {
Map<String, String> get withoutNulls => Map.fromEntries(
entries
.where((e) => e.value != null)
.map((e) => MapEntry(e.key, e.value!)),
);
}
extension NavigationExtensions on BuildContext {
void goNamedAuth(
String name,
bool mounted, {
Map<String, String> pathParameters = const <String, String>{},
Map<String, String> queryParameters = const <String, String>{},
Object? extra,
bool ignoreRedirect = false,
}) =>
!mounted || GoRouter.of(this).shouldRedirect(ignoreRedirect)
? null
: goNamed(
name,
pathParameters: pathParameters,
queryParameters: queryParameters,
extra: extra,
);
void pushNamedAuth(
String name,
bool mounted, {
Map<String, String> pathParameters = const <String, String>{},
Map<String, String> queryParameters = const <String, String>{},
Object? extra,
bool ignoreRedirect = false,
}) =>
!mounted || GoRouter.of(this).shouldRedirect(ignoreRedirect)
? null
: pushNamed(
name,
pathParameters: pathParameters,
queryParameters: queryParameters,
extra: extra,
);
void safePop() {
// If there is only one route on the stack, navigate to the initial
// page instead of popping.
if (canPop()) {
pop();
} else {
go('/');
}
}
}
extension GoRouterExtensions on GoRouter {
AppStateNotifier get appState => AppStateNotifier.instance;
void prepareAuthEvent([bool ignoreRedirect = false]) =>
appState.hasRedirect() && !ignoreRedirect
? null
: appState.updateNotifyOnAuthChange(false);
bool shouldRedirect(bool ignoreRedirect) =>
!ignoreRedirect && appState.hasRedirect();
void clearRedirectLocation() => appState.clearRedirectLocation();
void setRedirectLocationIfUnset(String location) =>
appState.updateNotifyOnAuthChange(false);
}
extension _GoRouterStateExtensions on GoRouterState {
Map<String, dynamic> get extraMap =>
extra != null ? extra as Map<String, dynamic> : {};
Map<String, dynamic> get allParams => <String, dynamic>{}
..addAll(pathParameters)
..addAll(queryParameters)
..addAll(extraMap);
TransitionInfo get transitionInfo => extraMap.containsKey(kTransitionInfoKey)
? extraMap[kTransitionInfoKey] as TransitionInfo
: TransitionInfo.appDefault();
}
class FFParameters {
FFParameters(this.state, [this.asyncParams = const {}]);
final GoRouterState state;
final Map<String, Future<dynamic> Function(String)> asyncParams;
Map<String, dynamic> futureParamValues = {};
// Parameters are empty if the params map is empty or if the only parameter
// present is the special extra parameter reserved for the transition info.
bool get isEmpty =>
state.allParams.isEmpty ||
(state.extraMap.length == 1 &&
state.extraMap.containsKey(kTransitionInfoKey));
bool isAsyncParam(MapEntry<String, dynamic> param) =>
asyncParams.containsKey(param.key) && param.value is String;
bool get hasFutures => state.allParams.entries.any(isAsyncParam);
Future<bool> completeFutures() => Future.wait(
state.allParams.entries.where(isAsyncParam).map(
(param) async {
final doc = await asyncParams[param.key]!(param.value)
.onError((_, __) => null);
if (doc != null) {
futureParamValues[param.key] = doc;
return true;
}
return false;
},
),
).onError((_, __) => [false]).then((v) => v.every((e) => e));
dynamic getParam<T>(
String paramName,
ParamType type, [
bool isList = false,
]) {
if (futureParamValues.containsKey(paramName)) {
return futureParamValues[paramName];
}
if (!state.allParams.containsKey(paramName)) {
return null;
}
final param = state.allParams[paramName];
// Got parameter from `extras`, so just directly return it.
if (param is! String) {
return param;
}
// Return serialized value.
return deserializeParam<T>(
param,
type,
isList,
);
}
}
class FFRoute {
const FFRoute({
required this.name,
required this.path,
required this.builder,
this.requireAuth = false,
this.asyncParams = const {},
this.routes = const [],
});
final String name;
final String path;
final bool requireAuth;
final Map<String, Future<dynamic> Function(String)> asyncParams;
final Widget Function(BuildContext, FFParameters) builder;
final List<GoRoute> routes;
GoRoute toRoute(AppStateNotifier appStateNotifier) => GoRoute(
name: name,
path: path,
redirect: (context, state) {
if (appStateNotifier.shouldRedirect) {
final redirectLocation = appStateNotifier.getRedirectLocation();
appStateNotifier.clearRedirectLocation();
return redirectLocation;
}
if (requireAuth && !appStateNotifier.loggedIn) {
appStateNotifier.setRedirectLocationIfUnset(state.location);
return '/login';
}
return null;
},
pageBuilder: (context, state) {
final ffParams = FFParameters(state, asyncParams);
final page = ffParams.hasFutures
? FutureBuilder(
future: ffParams.completeFutures(),
builder: (context, _) => builder(context, ffParams),
)
: builder(context, ffParams);
final child = appStateNotifier.loading
? Center(
child: SizedBox(
width: 50.0,
height: 50.0,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
FlutterTheme.of(context).info,
),
),
),
)
: page;
final transitionInfo = state.transitionInfo;
return transitionInfo.hasTransition
? CustomTransitionPage(
key: state.pageKey,
child: child,
transitionDuration: transitionInfo.duration,
transitionsBuilder:
(context, animation, secondaryAnimation, child) =>
PageTransition(
type: transitionInfo.transitionType,
duration: transitionInfo.duration,
reverseDuration: transitionInfo.duration,
alignment: transitionInfo.alignment,
child: child,
).buildTransitions(
context,
animation,
secondaryAnimation,
child,
),
)
: MaterialPage(key: state.pageKey, child: child);
},
routes: routes,
);
}
class TransitionInfo {
const TransitionInfo({
required this.hasTransition,
this.transitionType = PageTransitionType.fade,
this.duration = const Duration(milliseconds: 300),
this.alignment,
});
final bool hasTransition;
final PageTransitionType transitionType;
final Duration duration;
final Alignment? alignment;
static TransitionInfo appDefault() => TransitionInfo(hasTransition: false);
}
class RootPageContext {
const RootPageContext(this.isRootPage, [this.errorRoute]);
final bool isRootPage;
final String? errorRoute;
static bool isInactiveRootPage(BuildContext context) {
final rootPageContext = context.read<RootPageContext?>();
final isRootPage = rootPageContext?.isRootPage ?? false;
final location = GoRouter.of(context).location;
return isRootPage &&
location != '/' &&
location != rootPageContext?.errorRoute;
}
static Widget wrap(Widget child, {String? errorRoute}) => Provider.value(
value: RootPageContext(true, errorRoute),
child: child,
);
}

View File

@@ -0,0 +1,213 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:from_css_color/from_css_color.dart';
import '/backend/schema/enums/enums.dart';
import '../../flutterlib/lat_lng.dart';
import '../../flutterlib/place.dart';
import '../../flutterlib/uploaded_file.dart';
/// SERIALIZATION HELPERS
String dateTimeRangeToString(DateTimeRange dateTimeRange) {
final startStr = dateTimeRange.start.millisecondsSinceEpoch.toString();
final endStr = dateTimeRange.end.millisecondsSinceEpoch.toString();
return '$startStr|$endStr';
}
String placeToString(FFPlace place) => jsonEncode({
'latLng': place.latLng.serialize(),
'name': place.name,
'address': place.address,
'city': place.city,
'state': place.state,
'country': place.country,
'zipCode': place.zipCode,
});
String uploadedFileToString(FFUploadedFile uploadedFile) =>
uploadedFile.serialize();
String? serializeParam(
dynamic param,
ParamType paramType, [
bool isList = false,
]) {
try {
if (param == null) {
return null;
}
if (isList) {
final serializedValues = (param as Iterable)
.map((p) => serializeParam(p, paramType, false))
.where((p) => p != null)
.map((p) => p!)
.toList();
return json.encode(serializedValues);
}
switch (paramType) {
case ParamType.int:
return param.toString();
case ParamType.double:
return param.toString();
case ParamType.String:
return param;
case ParamType.bool:
return param ? 'true' : 'false';
case ParamType.DateTime:
return (param as DateTime).millisecondsSinceEpoch.toString();
case ParamType.DateTimeRange:
return dateTimeRangeToString(param as DateTimeRange);
case ParamType.LatLng:
return (param as LatLng).serialize();
case ParamType.Color:
return (param as Color).toCssString();
case ParamType.FFPlace:
return placeToString(param as FFPlace);
case ParamType.FFUploadedFile:
return uploadedFileToString(param as FFUploadedFile);
case ParamType.JSON:
return json.encode(param);
case ParamType.Enum:
return (param is Enum) ? param.serialize() : null;
default:
return null;
}
} catch (e) {
print('Error serializing parameter: $e');
return null;
}
}
/// END SERIALIZATION HELPERS
/// DESERIALIZATION HELPERS
DateTimeRange? dateTimeRangeFromString(String dateTimeRangeStr) {
final pieces = dateTimeRangeStr.split('|');
if (pieces.length != 2) {
return null;
}
return DateTimeRange(
start: DateTime.fromMillisecondsSinceEpoch(int.parse(pieces.first)),
end: DateTime.fromMillisecondsSinceEpoch(int.parse(pieces.last)),
);
}
LatLng? latLngFromString(String latLngStr) {
final pieces = latLngStr.split(',');
if (pieces.length != 2) {
return null;
}
return LatLng(
double.parse(pieces.first.trim()),
double.parse(pieces.last.trim()),
);
}
FFPlace placeFromString(String placeStr) {
final serializedData = jsonDecode(placeStr) as Map<String, dynamic>;
final data = {
'latLng': serializedData.containsKey('latLng')
? latLngFromString(serializedData['latLng'] as String)
: const LatLng(0.0, 0.0),
'name': serializedData['name'] ?? '',
'address': serializedData['address'] ?? '',
'city': serializedData['city'] ?? '',
'state': serializedData['state'] ?? '',
'country': serializedData['country'] ?? '',
'zipCode': serializedData['zipCode'] ?? '',
};
return FFPlace(
latLng: data['latLng'] as LatLng,
name: data['name'] as String,
address: data['address'] as String,
city: data['city'] as String,
state: data['state'] as String,
country: data['country'] as String,
zipCode: data['zipCode'] as String,
);
}
FFUploadedFile uploadedFileFromString(String uploadedFileStr) =>
FFUploadedFile.deserialize(uploadedFileStr);
enum ParamType {
int,
double,
String,
bool,
DateTime,
DateTimeRange,
LatLng,
Color,
FFPlace,
FFUploadedFile,
JSON,
Enum,
}
dynamic deserializeParam<T>(
String? param,
ParamType paramType,
bool isList,
) {
try {
if (param == null) {
return null;
}
if (isList) {
final paramValues = json.decode(param);
if (paramValues is! Iterable || paramValues.isEmpty) {
return null;
}
return paramValues
.where((p) => p is String)
.map((p) => p as String)
.map((p) => deserializeParam<T>(p, paramType, false))
.where((p) => p != null)
.map((p) => p! as T)
.toList();
}
switch (paramType) {
case ParamType.int:
return int.tryParse(param);
case ParamType.double:
return double.tryParse(param);
case ParamType.String:
return param;
case ParamType.bool:
return param == 'true';
case ParamType.DateTime:
final milliseconds = int.tryParse(param);
return milliseconds != null
? DateTime.fromMillisecondsSinceEpoch(milliseconds)
: null;
case ParamType.DateTimeRange:
return dateTimeRangeFromString(param);
case ParamType.LatLng:
return latLngFromString(param);
case ParamType.Color:
return fromCssColor(param);
case ParamType.FFPlace:
return placeFromString(param);
case ParamType.FFUploadedFile:
return uploadedFileFromString(param);
case ParamType.JSON:
return json.decode(param);
case ParamType.Enum:
return deserializeEnum<T>(param);
default:
return null;
}
} catch (e) {
print('Error deserializing parameter: $e');
return null;
}
}

46
lib/flutterlib/place.dart Normal file
View File

@@ -0,0 +1,46 @@
import 'lat_lng.dart';
class FFPlace {
const FFPlace({
this.latLng = const LatLng(0.0, 0.0),
this.name = '',
this.address = '',
this.city = '',
this.state = '',
this.country = '',
this.zipCode = '',
});
final LatLng latLng;
final String name;
final String address;
final String city;
final String state;
final String country;
final String zipCode;
@override
String toString() => '''FFPlace(
latLng: $latLng,
name: $name,
address: $address,
city: $city,
state: $state,
country: $country,
zipCode: $zipCode,
)''';
@override
int get hashCode => latLng.hashCode;
@override
bool operator ==(other) =>
other is FFPlace &&
latLng == other.latLng &&
name == other.name &&
address == other.address &&
city == other.city &&
state == other.state &&
country == other.country &&
zipCode == other.zipCode;
}

View File

@@ -0,0 +1,378 @@
import 'dart:async';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mime_type/mime_type.dart';
import 'package:video_player/video_player.dart';
import 'flutter_theme.dart';
import 'flutter_util.dart';
const allowedFormats = {'image/png', 'image/jpeg', 'video/mp4', 'image/gif'};
class SelectedFile {
const SelectedFile({
this.storagePath = '',
this.filePath,
required this.bytes,
this.dimensions,
this.blurHash,
});
final String storagePath;
final String? filePath;
final Uint8List bytes;
final MediaDimensions? dimensions;
final String? blurHash;
}
class MediaDimensions {
const MediaDimensions({
this.height,
this.width,
});
final double? height;
final double? width;
}
enum MediaSource {
photoGallery,
videoGallery,
camera,
}
Future<List<SelectedFile>?> selectMediaWithSourceBottomSheet({
required BuildContext context,
String? storageFolderPath,
double? maxWidth,
double? maxHeight,
int? imageQuality,
required bool allowPhoto,
bool allowVideo = false,
String pickerFontFamily = 'Roboto',
Color textColor = const Color(0xFF111417),
Color backgroundColor = const Color(0xFFF5F5F5),
bool includeDimensions = false,
bool includeBlurHash = false,
}) async {
final createUploadMediaListTile =
(String label, MediaSource mediaSource) => ListTile(
title: Text(
label,
textAlign: TextAlign.center,
style: GoogleFonts.getFont(
pickerFontFamily,
color: textColor,
fontWeight: FontWeight.w600,
fontSize: 20,
),
),
tileColor: backgroundColor,
dense: false,
onTap: () => Navigator.pop(
context,
mediaSource,
),
);
final mediaSource = await showModalBottomSheet<MediaSource>(
context: context,
backgroundColor: backgroundColor,
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!kIsWeb) ...[
Padding(
padding: EdgeInsets.fromLTRB(0, 8, 0, 0),
child: ListTile(
title: Text(
'Choose Source',
textAlign: TextAlign.center,
style: GoogleFonts.getFont(
pickerFontFamily,
color: textColor.withOpacity(0.65),
fontWeight: FontWeight.w500,
fontSize: 20,
),
),
tileColor: backgroundColor,
dense: false,
),
),
const Divider(),
],
if (allowPhoto && allowVideo) ...[
createUploadMediaListTile(
'Gallery (Photo)',
MediaSource.photoGallery,
),
const Divider(),
createUploadMediaListTile(
'Gallery (Video)',
MediaSource.videoGallery,
),
] else if (allowPhoto)
createUploadMediaListTile(
'Gallery',
MediaSource.photoGallery,
)
else
createUploadMediaListTile(
'Gallery',
MediaSource.videoGallery,
),
if (!kIsWeb) ...[
const Divider(),
createUploadMediaListTile('Camera', MediaSource.camera),
const Divider(),
],
const SizedBox(height: 10),
],
);
});
if (mediaSource == null) {
return null;
}
return selectMedia(
storageFolderPath: storageFolderPath,
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: imageQuality,
isVideo: mediaSource == MediaSource.videoGallery ||
(mediaSource == MediaSource.camera && allowVideo && !allowPhoto),
mediaSource: mediaSource,
includeDimensions: includeDimensions,
includeBlurHash: includeBlurHash,
);
}
Future<List<SelectedFile>?> selectMedia({
String? storageFolderPath,
double? maxWidth,
double? maxHeight,
int? imageQuality,
bool isVideo = false,
MediaSource mediaSource = MediaSource.camera,
bool multiImage = false,
bool includeDimensions = false,
bool includeBlurHash = false,
}) async {
final picker = ImagePicker();
if (multiImage) {
final pickedMediaFuture = picker.pickMultiImage(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: imageQuality,
);
final pickedMedia = await pickedMediaFuture;
if (pickedMedia.isEmpty) {
return null;
}
return Future.wait(pickedMedia.asMap().entries.map((e) async {
final index = e.key;
final media = e.value;
final mediaBytes = await media.readAsBytes();
final path = _getStoragePath(storageFolderPath, media.name, false, index);
final dimensions = includeDimensions
? isVideo
? _getVideoDimensions(media.path)
: _getImageDimensions(mediaBytes)
: null;
return SelectedFile(
storagePath: path,
filePath: media.path,
bytes: mediaBytes,
dimensions: await dimensions,
);
}));
}
final source = mediaSource == MediaSource.camera
? ImageSource.camera
: ImageSource.gallery;
final pickedMediaFuture = isVideo
? picker.pickVideo(source: source)
: picker.pickImage(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: imageQuality,
source: source,
);
final pickedMedia = await pickedMediaFuture;
final mediaBytes = await pickedMedia?.readAsBytes();
if (mediaBytes == null) {
return null;
}
final path = _getStoragePath(storageFolderPath, pickedMedia!.name, isVideo);
final dimensions = includeDimensions
? isVideo
? _getVideoDimensions(pickedMedia.path)
: _getImageDimensions(mediaBytes)
: null;
return [
SelectedFile(
storagePath: path,
filePath: pickedMedia.path,
bytes: mediaBytes,
dimensions: await dimensions,
),
];
}
bool validateFileFormat(String filePath, BuildContext context) {
if (allowedFormats.contains(mime(filePath))) {
return true;
}
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(
content: Text('Invalid file format: ${mime(filePath)}'),
));
return false;
}
Future<SelectedFile?> selectFile({
String? storageFolderPath,
List<String>? allowedExtensions,
}) =>
selectFiles(
storageFolderPath: storageFolderPath,
allowedExtensions: allowedExtensions,
multiFile: false,
).then((value) => value?.first);
Future<List<SelectedFile>?> selectFiles({
String? storageFolderPath,
List<String>? allowedExtensions,
bool multiFile = false,
}) async {
final pickedFiles = await FilePicker.platform.pickFiles(
type: allowedExtensions != null ? FileType.custom : FileType.any,
allowedExtensions: allowedExtensions,
withData: true,
allowMultiple: multiFile,
);
if (pickedFiles == null || pickedFiles.files.isEmpty) {
return null;
}
if (multiFile) {
return Future.wait(pickedFiles.files.asMap().entries.map((e) async {
final index = e.key;
final file = e.value;
final storagePath =
_getStoragePath(storageFolderPath, file.name, false, index);
return SelectedFile(
storagePath: storagePath,
filePath: isWeb ? null : file.path,
bytes: file.bytes!,
);
}));
}
final file = pickedFiles.files.first;
if (file.bytes == null) {
return null;
}
final storagePath = _getStoragePath(storageFolderPath, file.name, false);
return [
SelectedFile(
storagePath: storagePath,
filePath: isWeb ? null : file.path,
bytes: file.bytes!,
)
];
}
List<SelectedFile> selectedFilesFromUploadedFiles(
List<FFUploadedFile> uploadedFiles, {
String? storageFolderPath,
bool isMultiData = false,
}) =>
uploadedFiles.asMap().entries.map(
(entry) {
final index = entry.key;
final file = entry.value;
return SelectedFile(
storagePath: _getStoragePath(
storageFolderPath != null ? storageFolderPath : null,
file.name!,
false,
isMultiData ? index : null,
),
bytes: file.bytes!);
},
).toList();
Future<MediaDimensions> _getImageDimensions(Uint8List mediaBytes) async {
final image = await decodeImageFromList(mediaBytes);
return MediaDimensions(
width: image.width.toDouble(),
height: image.height.toDouble(),
);
}
Future<MediaDimensions> _getVideoDimensions(String path) async {
final VideoPlayerController videoPlayerController =
VideoPlayerController.asset(path);
await videoPlayerController.initialize();
final size = videoPlayerController.value.size;
return MediaDimensions(width: size.width, height: size.height);
}
String _getStoragePath(
String? pathPrefix,
String filePath,
bool isVideo, [
int? index,
]) {
pathPrefix = _removeTrailingSlash(pathPrefix);
final timestamp = DateTime.now().microsecondsSinceEpoch;
// Workaround fixed by https://github.com/flutter/plugins/pull/3685
// (not yet in stable).
final ext = isVideo ? 'mp4' : filePath.split('.').last;
final indexStr = index != null ? '_$index' : '';
return '$pathPrefix/$timestamp$indexStr.$ext';
}
String getSignatureStoragePath([String? pathPrefix]) {
pathPrefix = _removeTrailingSlash(pathPrefix);
final timestamp = DateTime.now().microsecondsSinceEpoch;
return '$pathPrefix/signature_$timestamp.png';
}
void showUploadMessage(
BuildContext context,
String message, {
bool showLoading = false,
}) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Row(
children: [
if (showLoading)
Padding(
padding: EdgeInsetsDirectional.only(end: 10.0),
child: CircularProgressIndicator(
valueColor: Theme.of(context).brightness == Brightness.dark
? AlwaysStoppedAnimation<Color>(
FlutterTheme.of(context).accent4)
: null,
),
),
Text(message),
],
),
duration: showLoading ? Duration(days: 1) : Duration(seconds: 4),
),
);
}
String? _removeTrailingSlash(String? path) => path != null && path.endsWith('/')
? path.substring(0, path.length - 1)
: path;

View File

@@ -0,0 +1,68 @@
import 'dart:convert';
import 'dart:typed_data' show Uint8List;
class FFUploadedFile {
const FFUploadedFile({
this.name,
this.bytes,
this.height,
this.width,
this.blurHash,
});
final String? name;
final Uint8List? bytes;
final double? height;
final double? width;
final String? blurHash;
@override
String toString() =>
'FFUploadedFile(name: $name, bytes: ${bytes?.length ?? 0}, height: $height, width: $width, blurHash: $blurHash,)';
String serialize() => jsonEncode(
{
'name': name,
'bytes': bytes,
'height': height,
'width': width,
'blurHash': blurHash,
},
);
static FFUploadedFile deserialize(String val) {
final serializedData = jsonDecode(val) as Map<String, dynamic>;
final data = {
'name': serializedData['name'] ?? '',
'bytes': serializedData['bytes'] ?? Uint8List.fromList([]),
'height': serializedData['height'],
'width': serializedData['width'],
'blurHash': serializedData['blurHash'],
};
return FFUploadedFile(
name: data['name'] as String,
bytes: Uint8List.fromList(data['bytes'].cast<int>().toList()),
height: data['height'] as double?,
width: data['width'] as double?,
blurHash: data['blurHash'] as String?,
);
}
@override
int get hashCode => Object.hash(
name,
bytes,
height,
width,
blurHash,
);
@override
bool operator ==(other) =>
other is FFUploadedFile &&
name == other.name &&
bytes == other.bytes &&
height == other.height &&
width == other.width &&
blurHash == other.blurHash;
}