基于flutterflow-ui v0.3.1版本 去除google ad 与google map

This commit is contained in:
khlu
2024-08-10 12:42:14 +08:00
parent f0c029ed7c
commit e9197e14ba
48 changed files with 7871 additions and 2 deletions

4
lib/flutterflow_ui.dart Normal file
View File

@@ -0,0 +1,4 @@
library flutterflow_ui;
export 'src/utils/flutter_flow_utils.dart';
export 'src/widgets/flutter_flow_widgets.dart';

155
lib/src/constants.dart Normal file
View File

@@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
/// Constants that we should use throughout the app for widget dimension/stylings.
/// Dimensions
const double kPadding2px = 2.0;
const double kPadding4px = 4.0;
const double kPadding8px = 8.0;
const double kPadding12px = 12.0;
const double kPadding16px = 16.0;
const double kPadding20px = 20.0;
const double kPadding24px = 24.0;
const double kPadding28px = 28.0;
const double kBaseSize8px = 8.0;
const double kBaseSize12px = 12.0;
const double kBaseSize16px = 16.0;
const double kBaseSize20px = 20.0;
const double kBaseSize24px = 24.0;
const double kBaseSize28px = 28.0;
const double kBaseSize32px = 32.0;
const double kBaseSize36px = 36.0;
const double kBaseSize48px = 48.0;
const double kBaseSize60px = 60.0;
const double kBaseSize72px = 72.0;
const double kBaseSize96px = 96.0;
const double kBaseSize120px = 120.0;
const double kBaseSize160px = 160.0;
const double kWidth48px = 48.0;
const double kWidth56px = 56.0;
//* Width for a property editor that is a quarter of the width of panel.
const double kWidth60px = 60.0;
const double kWidth64px = 64.0;
const double kWidth72px = 72.0;
const double kWidth80px = 80.0;
const double kWidth96px = 96.0;
const double kWidth108px = 108.0;
const double kWidth120px = 120.0;
//* Width for a property editor that is half the width of panel.
const double kWidth132px = 132.0;
const double kWidth144px = 144.0;
const double kWidth160px = 160.0;
//* Width for a property editor that is total the width of panel.
const double kWidth276px = 276.0;
const double kWidth440px = 440.0;
const double kWidth480px = 480.0;
const double kWidth600px = 600.0;
const double kWidth720px = 720.0;
const double kWidth840px = 840.0;
const double kHeight16px = 16.0;
const double kHeight20px = 20.0;
const double kHeight24px = 24.0;
const double kHeight28px = 28.0;
//* Typically used for input fields' height.
const double kHeight32px = 32.0;
const double kHeight36px = 36.0;
const double kHeight40px = 40.0;
const double kHeight48px = 48.0;
const double kHeight56px = 56.0;
const double kHeight64px = 64.0;
const double kHeight72px = 72.0;
const double kHeight80px = 80.0;
const double kHeight88px = 88.0;
const double kHeight96px = 96.0;
const double kHeight240px = 240.0;
const double kHeight320px = 320.0;
const double kHeight400px = 400.0;
const double kHeight480px = 480.0;
/// Border Radius
const double kBorderRadius4px = 4.0;
const double kBorderRadius8px = 8.0;
const double kBorderRadius12px = 12.0;
/// Border Width
const double kBorderWidth1px = 1.0;
const double kBorderWidth1_5px = 1.5;
const double kBorderWidth2px = 2.0;
/// Line Width
const double kLineWidth1px = 1.0;
const double kLineWidth2px = 2.0;
const double kLineWidth4px = 4.0;
//* Typically used for icon size in property name line.
const double kIconSize14px = 14.0;
const double kIconSize16px = 16.0;
const double kIconSize20px = 20.0;
const double kIconSize22px = 22.0;
const double kIconSize24px = 24.0;
const double kIconSize32px = 32.0;
/// Durations
const Duration kDuration250ms = Duration(milliseconds: 250);
const Duration kDuration500ms = Duration(milliseconds: 500);
/// Durations
const Curve kCurveEase = Curves.ease;
/// Elevations and shadowing
const double kElevation0 = 0.0;
const double kElevation2 = 2.0;
const double kElevation4 = 4.0;
const double kElevation8 = 8.0;
final kBoxShadow2 = kElevationToShadow[2]!;
final kBoxShadow4 = kElevationToShadow[4]!;
final kBoxShadow8 = kElevationToShadow[8]!;
/// Blur
const double kBlur4 = 4.0;
const double kBlur8 = 8.0;
/// Opacity
const double kOpacity0_2 = 0.2;
const double kOpacity0_4 = 0.4;
const double kOpacity0_6 = 0.6;
const double kOpacity0_8 = 0.8;
/// Color
const kErrorColor = Color(0xFFDF3F3F);
const kPrimaryColor = Color(0xFF4B39EF);
const kSecondaryColor = Color(0xFFEE8B60);
const kTertiaryColor = Color(0xFF1D2429);
const kSuccessToastColor = Color(0xFF39D2C0);
const kGrey250 = Color(0xFFE3E7ED);
const kGrey800 = Color(0xFF424E5A);
const kDiffAddedColor = Colors.green;
const kDiffDeletedColor = Color.fromARGB(255, 255, 129, 129);
/// Font Sizes
const double kFontSize12px = 12.0;
const double kFontSize13px = 13.0;
const double kFontSize14px = 14.0;
const double kFontSize16px = 16.0;
const double kFontSize18px = 18.0;
const double kFontSize20px = 20.0;
const double kFontSize24px = 24.0;
const double kFontSize32px = 32.0;
const double kFontSize36px = 36.0;

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
enum AnimationTrigger {
onPageLoad,
onActionTrigger,
}
class AnimationInfo {
AnimationInfo({
required this.trigger,
required this.effectsBuilder,
this.loop = false,
this.reverse = false,
this.applyInitialState = true,
});
final AnimationTrigger trigger;
final List<Effect> Function()? effectsBuilder;
final bool applyInitialState;
final bool loop;
final bool reverse;
late AnimationController controller;
List<Effect>? _effects;
List<Effect> get effects => _effects ??= effectsBuilder!();
void maybeUpdateEffects(List<Effect>? updatedEffects) {
if (updatedEffects != null) {
_effects = updatedEffects;
}
}
}
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, {
List<Effect>? effects,
}) {
animationInfo.maybeUpdateEffects(effects);
return 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, {
List<Effect>? effects,
bool hasBeenTriggered = false,
}) {
animationInfo.maybeUpdateEffects(effects);
return hasBeenTriggered || animationInfo.applyInitialState
? Animate(controller: animationInfo.controller, autoPlay: false, effects: animationInfo.effects, child: this)
: this;
}
}
class TiltEffect extends Effect<Offset> {
const TiltEffect({
super.delay,
super.duration,
super.curve,
Offset? begin,
Offset? end,
}) : super(
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,279 @@
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;
export 'dart:convert' show jsonEncode, jsonDecode;
export 'dart:math' show min, max;
export 'dart:typed_data' show Uint8List;
export 'package:intl/intl.dart';
export 'package:page_transition/page_transition.dart';
export 'flutter_flow_model.dart';
export 'lat_lng.dart';
export 'place.dart';
final RouteObserver<ModalRoute> routeObserver = RouteObserver<PageRoute>();
T valueOrDefault<T>(T? value, T defaultValue) =>
(value is String && value.isEmpty) || value == null ? defaultValue : value;
String dateTimeFormat(String format, DateTime? dateTime, {String? locale}) {
if (dateTime == null) {
return '';
}
if (format == 'relative') {
return timeago.format(dateTime, locale: locale, allowFromNow: true);
}
return DateFormat(format, locale).format(dateTime);
}
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);
}
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;
return isForList && value is! Iterable ? [value] : 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<S> mapIndexed<S>(S Function(int, T) func) => toList()
.asMap()
.map((index, value) => MapEntry(index, func(index, value)))
.values
.toList();
}
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)
const Padding(
padding: EdgeInsetsDirectional.only(end: 10.0),
child: SizedBox(
height: 20,
width: 20,
child: 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,172 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
Widget wrapWithModel<T extends FlutterFlowModel>({
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.
..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 FlutterFlowModel>(
BuildContext context,
T Function() defaultBuilder,
) {
final model = context.read<T?>() ?? defaultBuilder()
.._init(context);
return model;
}
abstract class FlutterFlowModel<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() : () {};
FlutterFlowModel 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 FlutterFlowDynamicModels<T extends FlutterFlowModel> {
FlutterFlowDynamicModels(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)?.maybeDispose());
_activeKeys = null;
});
}
}
}
T? _getDefaultValue<T>() {
switch (T) {
case const (int):
return 0 as T;
case const (double):
return 0.0 as T;
case const (String):
return '' as T;
case const (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,71 @@
import 'package:flutter/foundation.dart';
import 'package:rive/rive.dart';
class FlutterFlowRiveController extends SimpleAnimation {
FlutterFlowRiveController(
super.animationName, {
super.mix,
super.autoplay,
this.shouldLoop = false,
});
bool shouldLoop;
final _reactivate = ValueNotifier<bool>(false);
ValueListenable<bool> get changeReactivate => _reactivate;
bool get reactivate => _reactivate.value;
set reactivate(bool value) {
if (_reactivate.value != value) {
_reactivate.value = value;
}
}
bool endOfAnimation(LinearAnimationInstance? instance) {
if (instance == null) {
return false;
}
return instance.time == instance.animation.endTime;
}
@override
bool init(RuntimeArtboard artboard) {
reactivate = false;
changeReactivate.addListener(() {
if (reactivate) {
isActive = true;
}
});
return super.init(artboard);
}
@override
void apply(RuntimeArtboard artboard, double elapsedSeconds) {
if (instance == null) {
return;
}
/// Reset on button press
if (reactivate) {
if (endOfAnimation(instance)) {
instance?.time = 0;
}
reactivate = false;
}
if (instance == null || endOfAnimation(instance)) {
isActive = false;
}
/// Stop after one loop if not a continuous animation
if (!shouldLoop &&
(instance?.animation.loop == Loop.loop ||
instance?.animation.loop == Loop.pingPong) &&
instance!.didLoop) {
isActive = false;
}
instance!
..animation.apply(instance!.time, coreContext: artboard, mix: mix)
..advance(elapsedSeconds);
}
}

View File

@@ -0,0 +1,11 @@
library flutterflow_ui;
export 'flutter_flow_animations.dart';
export 'flutter_flow_helpers.dart';
export 'flutter_flow_rive_controller.dart';
export 'form_field_controller.dart';
export 'internationalization.dart';
export 'lat_lng.dart';
export 'place.dart';
export 'random_data.dart';
export 'uploaded_file.dart';

View File

@@ -0,0 +1,24 @@
import 'package:flutter/foundation.dart';
class FormFieldController<T> extends ValueNotifier<T?> {
FormFieldController(this.initialValue) : super(initialValue);
final T? initialValue;
void reset() => value = initialValue;
void update() => notifyListeners();
}
// If the initial value is a list (which it is for multiselect),
// we need to use this controller to avoid a pass by reference issue
// that can result in the initial value being modified.
class FormListFieldController<T> extends FormFieldController<List<T>> {
FormListFieldController(super.initialValue)
: _initialListValue = List<T>.from(initialValue ?? []);
final List<T>? _initialListValue;
@override
void reset() => value = List<T>.from(_initialListValue ?? []);
}

View File

@@ -0,0 +1,47 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
//Helper class for internationalization
class FFLocalizations {
FFLocalizations(this.locale);
final Locale locale;
//Initializing
static FFLocalizations of(BuildContext context) =>
FFLocalizations(const Locale('en'));
static List<String> languages() => ['en'];
String get languageCode => locale.languageCode;
int get languageIndex => languages().contains(languageCode)
? languages().indexOf(languageCode)
: 0;
String getText(String key) =>
(kTranslationsMap[key] ?? {})[locale.languageCode] ?? '';
String getVariableText({
String? enText = '',
}) =>
[enText][languageIndex] ?? '';
}
class FFLocalizationsDelegate extends LocalizationsDelegate<FFLocalizations> {
const FFLocalizationsDelegate();
@override
bool isSupported(Locale locale) =>
FFLocalizations.languages().contains(locale.languageCode);
@override
Future<FFLocalizations> load(Locale locale) =>
SynchronousFuture<FFLocalizations>(FFLocalizations(locale));
@override
bool shouldReload(FFLocalizationsDelegate old) => false;
}
final kTranslationsMap =
<Map<String, Map<String, String>>>[].reduce((a, b) => a..addAll(b));

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;
}

46
lib/src/utils/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,51 @@
import 'dart:math';
import 'package:flutter/material.dart';
final _random = Random();
int randomInteger(int min, int max) {
return _random.nextInt(max - min + 1) + min;
}
double randomDouble(double min, double max) {
return _random.nextDouble() * (max - min) + min;
}
String randomString(
int minLength,
int maxLength,
bool lowercaseAz,
bool uppercaseAz,
bool digits,
) {
var chars = '';
if (lowercaseAz) {
chars += 'abcdefghijklmnopqrstuvwxyz';
}
if (uppercaseAz) {
chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
}
if (digits) {
chars += '0123456789';
}
return List.generate(randomInteger(minLength, maxLength),
(index) => chars[_random.nextInt(chars.length)]).join();
}
// Random date between 1970 and 2025.
DateTime randomDate() {
// Random max must be in range 0 < max <= 2^32.
// So we have to generate the time in seconds and then convert to milliseconds.
return DateTime.fromMillisecondsSinceEpoch(
randomInteger(0, 1735689600) * 1000);
}
String randomImageUrl(int width, int height) {
return 'https://picsum.photos/seed/${_random.nextInt(1000)}/$width/$height';
}
Color randomColor() {
return Color.fromARGB(
255, _random.nextInt(255), _random.nextInt(255), _random.nextInt(255));
}

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;
}

View File

@@ -0,0 +1,148 @@
// import 'dart:io';
// import 'package:flutter/foundation.dart' show kIsWeb;
// import 'package:flutter/material.dart';
// import 'package:flutter/scheduler.dart';
// import 'package:google_mobile_ads/google_mobile_ads.dart';
// /// A widget that displays a banner ad.
// class FlutterFlowAdBanner extends StatefulWidget {
// const FlutterFlowAdBanner({
// super.key,
// this.width,
// this.height,
// required this.showsTestAd,
// this.iOSAdUnitID,
// this.androidAdUnitID,
// });
// /// The width of the ad banner.
// final double? width;
// /// The height of the ad banner.
// final double? height;
// /// Whether to show a test ad.
// final bool showsTestAd;
// /// The Ad Unit ID for iOS.
// final String? iOSAdUnitID;
// /// The Ad Unit ID for Android.
// final String? androidAdUnitID;
// @override
// State<FlutterFlowAdBanner> createState() => _FlutterFlowAdBannerState();
// }
// class _FlutterFlowAdBannerState extends State<FlutterFlowAdBanner> {
// static const AdRequest request = AdRequest();
// BannerAd? _anchoredBanner;
// AdWidget? adWidget;
// @override
// void initState() {
// super.initState();
// SchedulerBinding.instance.addPostFrameCallback((_) {
// _createAnchoredBanner(context);
// });
// }
// @override
// void dispose() {
// super.dispose();
// _anchoredBanner?.dispose();
// }
// @override
// Widget build(BuildContext context) {
// var loadingText = 'Ad Loading... \\n\\n';
// if (widget.showsTestAd) {
// loadingText +=
// 'If this takes a long time, you may have to check whether the ad is '
// 'being covered from a parent widget. For example, a larger width than '
// 'the device screen size or a large border radius encompassing the ad banner '
// 'may stop ads from loading.\\n\\n'
// 'If a full-width banner is desired for your app, leave the width and '
// 'height of the AdBanner widget empty. AdBanner will automatically'
// 'match the size of the banner to the device screen.';
// }
// return _anchoredBanner != null && adWidget != null
// ? Container(
// alignment: Alignment.center,
// color: Colors.red,
// width: _anchoredBanner!.size.width.toDouble(),
// height: _anchoredBanner!.size.height.toDouble(),
// child: adWidget,
// )
// : Container(
// color: Colors.black,
// alignment: Alignment.center,
// child: Padding(
// padding: const EdgeInsets.all(8.0),
// child: Text(
// loadingText,
// style: const TextStyle(
// fontSize: 10.0,
// color: Colors.white,
// ),
// ),
// ),
// );
// }
// /// Creates an anchored banner ad.
// Future _createAnchoredBanner(BuildContext context) async {
// final AdSize? size = widget.width != null && widget.height != null
// ? AdSize(
// height: widget.height!.toInt(),
// width: widget.width!.toInt(),
// )
// : await AdSize.getAnchoredAdaptiveBannerAdSize(
// widget.width == null ? Orientation.portrait : Orientation.landscape,
// widget.width == null
// ? MediaQuery.sizeOf(context).width.truncate()
// : MediaQuery.sizeOf(context).height.truncate(),
// );
// if (size == null) {
// print('Unable to get size of anchored banner.');
// return;
// }
// final isAndroid = !kIsWeb && Platform.isAndroid;
// final BannerAd banner = BannerAd(
// size: size,
// request: request,
// adUnitId: widget.showsTestAd
// ? isAndroid
// ? 'ca-app-pub-3940256099942544/6300978111'
// : 'ca-app-pub-3940256099942544/2934735716'
// : isAndroid
// ? widget.androidAdUnitID!
// : widget.iOSAdUnitID!,
// listener: BannerAdListener(
// onAdLoaded: (ad) {
// print('\$BannerAd loaded.');
// if (mounted) {
// setState(() => _anchoredBanner = ad as BannerAd);
// }
// },
// onAdFailedToLoad: (ad, error) {
// print('\$BannerAd failedToLoad: \$error');
// ad.dispose();
// },
// onAdOpened: (ad) => print('\$BannerAd onAdOpened.'),
// onAdClosed: (ad) => print('\$BannerAd onAdClosed.'),
// ),
// );
// await banner.load();
// adWidget = AdWidget(ad: banner);
// setState(() {});
// return;
// }
// }

View File

@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:substring_highlight/substring_highlight.dart';
/// A widget that displays a list of autocomplete options for a text field.
class AutocompleteOptionsList extends StatelessWidget {
const AutocompleteOptionsList({
super.key,
required this.textFieldKey,
required this.textController,
required this.options,
required this.onSelected,
required this.textStyle,
this.textAlign = TextAlign.start,
this.optionBackgroundColor,
this.optionHighlightColor,
this.textHighlightStyle,
this.maxHeight,
this.elevation = 4.0,
});
/// The key of the text field associated with the autocomplete options list.
final GlobalKey textFieldKey;
/// The controller for the text field.
final TextEditingController textController;
/// The list of autocomplete options.
final List<String> options;
/// The callback function that is called when an option is selected.
final Function(String) onSelected;
/// The color used to highlight the selected option.
final Color? optionHighlightColor;
/// The background color of the options.
final Color? optionBackgroundColor;
/// The style of the text in the options.
final TextStyle textStyle;
/// The style of the highlighted text in the options.
final TextStyle? textHighlightStyle;
/// The alignment of the text in the options.
final TextAlign textAlign;
/// The maximum height of the options list.
final double? maxHeight;
/// The elevation of the options list.
final double elevation;
@override
Widget build(BuildContext context) {
final textFieldBox =
textFieldKey.currentContext!.findRenderObject() as RenderBox;
final textFieldWidth = textFieldBox.size.width;
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: elevation,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: textFieldWidth,
maxHeight: maxHeight ?? 200,
),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (context, index) {
final option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: Builder(builder: (context) {
final bool highlight =
AutocompleteHighlightedOption.of(context) == index;
if (highlight) {
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
Scrollable.ensureVisible(context, alignment: 0.5);
});
}
return Container(
color: highlight
? optionHighlightColor ?? Theme.of(context).focusColor
: optionBackgroundColor,
padding: const EdgeInsets.all(16.0),
child: SubstringHighlight(
text: option,
term: textController.text,
textStyle: textStyle,
textAlign: textAlign,
textStyleHighlight: textHighlightStyle ?? textStyle,
),
);
}),
);
},
),
),
),
);
}
}

View File

@@ -0,0 +1,343 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class FFButtonOptions {
const FFButtonOptions({
this.textAlign,
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,
});
/// The alignment of the button's text within its bounds.
final TextAlign? textAlign;
/// The style of the button's text.
final TextStyle? textStyle;
/// The elevation of the button.
final double? elevation;
/// The height of the button.
final double? height;
/// The width of the button.
final double? width;
/// The padding around the button's content.
final EdgeInsetsGeometry? padding;
/// The background color of the button.
final Color? color;
/// The background color of the button when it is disabled.
final Color? disabledColor;
/// The text color of the button when it is disabled.
final Color? disabledTextColor;
/// The maximum number of lines for the button's text.
final int? maxLines;
/// The color of the splash effect when the button is pressed.
final Color? splashColor;
/// The size of the button's icon.
final double? iconSize;
/// The color of the button's icon.
final Color? iconColor;
/// The padding around the button's icon.
final EdgeInsetsGeometry? iconPadding;
/// The border radius of the button.
final BorderRadius? borderRadius;
/// The border of the button.
final BorderSide? borderSide;
/// The background color of the button when it is hovered.
final Color? hoverColor;
/// The border of the button when it is hovered.
final BorderSide? hoverBorderSide;
/// The text color of the button when it is hovered.
final Color? hoverTextColor;
/// The elevation of the button when it is hovered.
final double? hoverElevation;
}
/// A customizable button widget that can display text, an icon, and a loading indicator.
class FFButtonWidget extends StatefulWidget {
/// Creates a [FFButtonWidget].
///
/// - [text] parameter is required and specifies the text to be displayed on the button.
/// - [onPressed] parameter is a callback function that will be called when the button is pressed.
/// - [icon] parameter is an optional widget that can be used to display an icon alongside the text.
/// - [iconData] parameter is an optional icon data that can be used to display an icon alongside the text.
/// - [options] parameter is required and specifies the visual options for the button.
/// - [showLoadingIndicator] parameter is an optional boolean value that determines whether to show a loading indicator when the button is pressed.
const FFButtonWidget({
super.key,
required this.text,
required this.onPressed,
this.icon,
this.iconData,
required this.options,
this.showLoadingIndicator = true,
});
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;
String? get text =>
widget.options.textStyle?.fontSize == 0 ? null : widget.text;
@override
Widget build(BuildContext context) {
Widget textWidget = loading
? SizedBox(
width: widget.options.width == null
? _getTextWidth(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(
text ?? '',
style:
text == null ? null : widget.options.textStyle?.withoutColor(),
textAlign: widget.options.textAlign,
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: WidgetStateProperty.resolveWith<OutlinedBorder>(
(states) {
if (states.contains(WidgetState.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: WidgetStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(WidgetState.disabled) &&
widget.options.disabledTextColor != null) {
return widget.options.disabledTextColor;
}
if (states.contains(WidgetState.hovered) &&
widget.options.hoverTextColor != null) {
return widget.options.hoverTextColor;
}
return widget.options.textStyle?.color ?? Colors.white;
},
),
backgroundColor: WidgetStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(WidgetState.disabled) &&
widget.options.disabledColor != null) {
return widget.options.disabledColor;
}
if (states.contains(WidgetState.hovered) &&
widget.options.hoverColor != null) {
return widget.options.hoverColor;
}
return widget.options.color;
},
),
overlayColor: WidgetStateProperty.resolveWith<Color?>((states) {
if (states.contains(WidgetState.pressed)) {
return widget.options.splashColor;
}
return widget.options.hoverColor == null ? null : Colors.transparent;
}),
padding: WidgetStateProperty.all(widget.options.padding ??
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0)),
elevation: WidgetStateProperty.resolveWith<double?>(
(states) {
if (states.contains(WidgetState.hovered) &&
widget.options.hoverElevation != null) {
return widget.options.hoverElevation!;
}
return widget.options.elevation ?? 2.0;
},
),
);
if ((widget.icon != null || widget.iconData != null) && !loading) {
Widget icon = widget.icon ??
FaIcon(
widget.iconData!,
size: widget.options.iconSize,
color: widget.options.iconColor,
);
if (text == null) {
return Container(
height: widget.options.height,
width: widget.options.width,
decoration: BoxDecoration(
border: Border.fromBorderSide(
widget.options.borderSide ?? BorderSide.none,
),
borderRadius:
widget.options.borderRadius ?? BorderRadius.circular(8),
),
child: IconButton(
splashRadius: 1.0,
icon: Padding(
padding: widget.options.iconPadding ?? EdgeInsets.zero,
child: icon,
),
onPressed: onPressed,
style: style,
),
);
}
return SizedBox(
height: widget.options.height,
width: widget.options.width,
child: ElevatedButton.icon(
icon: Padding(
padding: widget.options.iconPadding ?? EdgeInsets.zero,
child: icon,
),
label: textWidget,
onPressed: onPressed,
style: style,
),
);
}
return SizedBox(
height: widget.options.height,
width: widget.options.width,
child: ElevatedButton(
onPressed: onPressed,
style: style,
child: textWidget,
),
);
}
}
/// Extension on [TextStyle] to create a new [TextStyle] without the color property.
/// This extension method returns a new [TextStyle] object with all properties of the original [TextStyle] except for the color property, which is set to null.
///
/// Example usage:
/// ```dart
/// TextStyle myTextStyle = TextStyle(color: Colors.red, fontSize: 16);
/// TextStyle newTextStyle = myTextStyle.withoutColor();
/// ```
extension _WithoutColorExtension on TextStyle {
/// Returns a new [TextStyle] object without the color property.
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,
overflow: overflow,
);
}
// Slightly hacky method of getting the layout width of the provided text.
double? _getTextWidth(String? text, TextStyle? style, int maxLines) =>
text != null
? (TextPainter(
text: TextSpan(text: text, style: style),
textDirection: TextDirection.ltr,
maxLines: maxLines,
)..layout())
.size
.width
: null;

View File

@@ -0,0 +1,854 @@
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 super.direction,
required super.mainAxisSize,
required super.mainAxisAlignment,
required super.crossAxisAlignment,
required TextDirection super.textDirection,
required super.verticalDirection,
required this.onPerformLayout,
});
_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 {
const _TabLabelBar({
required super.children,
required this.onPerformLayout,
}) : super(
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 super.physics,
required super.context,
required super.oldPosition,
required this.tabBar,
}) : super(
initialPixels: null,
);
final _FlutterFlowButtonTabBarState 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 _FlutterFlowButtonTabBarState tabBar;
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition? oldPosition) {
return _TabBarScrollPosition(
physics: physics,
context: context,
oldPosition: oldPosition,
tabBar: tabBar,
);
}
}
/// A Flutterflow Design widget that displays a horizontal row of tabs.
class FlutterFlowButtonTabBar 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 FlutterFlowButtonTabBar({
super.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,
});
/// 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 [FlutterFlowButtonTabBar] 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<FlutterFlowButtonTabBar> createState() =>
_FlutterFlowButtonTabBarState();
}
class _FlutterFlowButtonTabBarState extends State<FlutterFlowButtonTabBar>
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(FlutterFlowButtonTabBar 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, (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: WidgetStateProperty.all(
widget.useToggleButtonStyle ? 0 : widget.elevation),
/// give a pretty small minimum size
minimumSize: WidgetStateProperty.all(const Size(10, 10)),
padding: WidgetStateProperty.all(EdgeInsets.zero),
textStyle: WidgetStateProperty.all(textStyle),
foregroundColor: WidgetStateProperty.all(textColor),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: WidgetStateProperty.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) {
_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, (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,397 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:intl/intl.dart';
import 'package:table_calendar/table_calendar.dart';
DateTime kFirstDay = DateTime(1970, 1, 1);
DateTime kLastDay = DateTime(2100, 1, 1);
extension DateTimeExtension on DateTime {
DateTime get startOfDay => DateTime(year, month, day);
DateTime get endOfDay => DateTime(year, month, day, 23, 59);
}
/// A customizable calendar widget for FlutterFlow.
///
/// The `FlutterFlowCalendar` widget allows you to display a calendar with various customization options.
/// You can customize the color, date format, starting day of the week, header style, and more.
///
/// To use this widget, simply create an instance of `FlutterFlowCalendar` and pass in the desired parameters.
/// You can also provide a callback function to handle changes in the selected date range.
///
/// Example usage:
/// ```dart
/// FlutterFlowCalendar(
/// color: Colors.blue,
/// onChange: (DateTimeRange? selectedRange) {
/// // Handle selected date range change
/// },
/// initialDate: DateTime.now(),
/// weekFormat: true,
/// weekStartsMonday: true,
/// twoRowHeader: true,
/// iconColor: Colors.white,
/// dateStyle: TextStyle(fontSize: 16),
/// dayOfWeekStyle: TextStyle(fontWeight: FontWeight.bold),
/// inactiveDateStyle: TextStyle(color: Colors.grey),
/// selectedDateStyle: TextStyle(color: Colors.red),
/// titleStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
/// rowHeight: 40,
/// locale: 'en_US',
/// )
/// ```
class FlutterFlowCalendar extends StatefulWidget {
/// Creates a new instance of [FlutterFlowCalendar].
///
/// - `color` parameter specifies the background color of the calendar.
/// - `onChange` parameter is a callback function that will be called when the selected date range changes.
/// - `initialDate` parameter specifies the initial date to be displayed on the calendar.
/// - `weekFormat` parameter determines whether the calendar should be displayed in week format.
/// - `weekStartsMonday` parameter determines whether the week starts on Monday.
/// - `twoRowHeader` parameter determines whether the header should be displayed in two rows.
/// - `iconColor` parameter specifies the color of the icons used in the calendar.
/// - `dateStyle` parameter specifies the text style for the dates.
/// - `dayOfWeekStyle` parameter specifies the text style for the day of the week labels.
/// - `inactiveDateStyle` parameter specifies the text style for inactive dates.
/// - `selectedDateStyle` parameter specifies the text style for selected dates.
/// - `titleStyle` parameter specifies the text style for the calendar title.
/// - `rowHeight` parameter specifies the height of each row in the calendar.
/// - `locale` parameter specifies the locale to be used for formatting dates.
const FlutterFlowCalendar({
super.key,
required this.color,
this.onChange,
this.initialDate,
this.weekFormat = false,
this.weekStartsMonday = false,
this.twoRowHeader = false,
this.iconColor,
this.dateStyle,
this.dayOfWeekStyle,
this.inactiveDateStyle,
this.selectedDateStyle,
this.titleStyle,
this.rowHeight,
this.locale,
});
/// Determines whether the calendar should be displayed in week format.
final bool weekFormat;
/// Determines whether the week starts on Monday.
final bool weekStartsMonday;
/// Determines whether the header should have two rows.
final bool twoRowHeader;
/// The color of the calendar.
final Color color;
/// A callback function that is called when the selected date range changes.
final void Function(DateTimeRange?)? onChange;
/// The initial date to be displayed on the calendar.
final DateTime? initialDate;
/// The color of the icons in the calendar.
final Color? iconColor;
/// The text style for the dates displayed on the calendar.
final TextStyle? dateStyle;
/// The text style for the day of the week displayed on the calendar.
final TextStyle? dayOfWeekStyle;
/// The text style for the inactive dates on the calendar.
final TextStyle? inactiveDateStyle;
/// The text style for the selected dates on the calendar.
final TextStyle? selectedDateStyle;
/// The text style for the title of the calendar.
final TextStyle? titleStyle;
/// The height of each row in the calendar.
final double? rowHeight;
/// The locale to be used for formatting dates.
final String? locale;
@override
State<FlutterFlowCalendar> createState() => _FlutterFlowCalendarState();
}
class _FlutterFlowCalendarState extends State<FlutterFlowCalendar> {
late DateTime focusedDay;
late DateTime selectedDay;
late DateTimeRange selectedRange;
@override
void initState() {
super.initState();
focusedDay = widget.initialDate ?? DateTime.now();
selectedDay = widget.initialDate ?? DateTime.now();
selectedRange = DateTimeRange(
start: selectedDay.startOfDay,
end: selectedDay.endOfDay,
);
SchedulerBinding.instance
.addPostFrameCallback((_) => setSelectedDay(selectedRange.start));
}
CalendarFormat get calendarFormat =>
widget.weekFormat ? CalendarFormat.week : CalendarFormat.month;
StartingDayOfWeek get startingDayOfWeek => widget.weekStartsMonday
? StartingDayOfWeek.monday
: StartingDayOfWeek.sunday;
Color get color => widget.color;
Color get lightColor => widget.color.withOpacity(0.85);
Color get lighterColor => widget.color.withOpacity(0.60);
void setSelectedDay(
DateTime? newSelectedDay, [
DateTime? newSelectedEnd,
]) {
final newRange = newSelectedDay == null
? null
: DateTimeRange(
start: newSelectedDay.startOfDay,
end: newSelectedEnd ?? newSelectedDay.endOfDay,
);
setState(() {
selectedDay = newSelectedDay ?? selectedDay;
selectedRange = newRange ?? selectedRange;
if (widget.onChange != null) {
widget.onChange!(newRange);
}
});
}
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CalendarHeader(
focusedDay: focusedDay,
onLeftChevronTap: () => setState(
() => focusedDay = widget.weekFormat
? _previousWeek(focusedDay)
: _previousMonth(focusedDay),
),
onRightChevronTap: () => setState(
() => focusedDay = widget.weekFormat
? _nextWeek(focusedDay)
: _nextMonth(focusedDay),
),
onTodayButtonTap: () => setState(() => focusedDay = DateTime.now()),
titleStyle: widget.titleStyle,
iconColor: widget.iconColor,
locale: widget.locale,
twoRowHeader: widget.twoRowHeader,
),
TableCalendar(
focusedDay: focusedDay,
selectedDayPredicate: (date) => isSameDay(selectedDay, date),
firstDay: kFirstDay,
lastDay: kLastDay,
calendarFormat: calendarFormat,
headerVisible: false,
locale: widget.locale,
rowHeight: widget.rowHeight ?? MediaQuery.sizeOf(context).width / 7,
calendarStyle: CalendarStyle(
defaultTextStyle:
widget.dateStyle ?? const TextStyle(color: Color(0xFF5A5A5A)),
weekendTextStyle: widget.dateStyle ??
const TextStyle(color: Color(0xFF5A5A5A)),
holidayTextStyle: widget.dateStyle ??
const TextStyle(color: Color(0xFF5C6BC0)),
selectedTextStyle:
const TextStyle(color: Color(0xFFFAFAFA), fontSize: 16.0)
.merge(widget.selectedDateStyle),
todayTextStyle:
const TextStyle(color: Color(0xFFFAFAFA), fontSize: 16.0)
.merge(widget.selectedDateStyle),
outsideTextStyle: const TextStyle(color: Color(0xFF9E9E9E))
.merge(widget.inactiveDateStyle),
selectedDecoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
todayDecoration: BoxDecoration(
color: lighterColor,
shape: BoxShape.circle,
),
markerDecoration: BoxDecoration(
color: lightColor,
shape: BoxShape.circle,
),
markersMaxCount: 3,
canMarkersOverflow: true,
),
availableGestures: AvailableGestures.horizontalSwipe,
startingDayOfWeek: startingDayOfWeek,
daysOfWeekStyle: DaysOfWeekStyle(
weekdayStyle: const TextStyle(color: Color(0xFF616161))
.merge(widget.dayOfWeekStyle),
weekendStyle: const TextStyle(color: Color(0xFF616161))
.merge(widget.dayOfWeekStyle),
),
onPageChanged: (focused) {
if (focusedDay.startOfDay != focused.startOfDay) {
setState(() => focusedDay = focused);
}
},
onDaySelected: (newSelectedDay, focused) {
if (!isSameDay(selectedDay, newSelectedDay)) {
setSelectedDay(newSelectedDay);
if (focusedDay.startOfDay != focused.startOfDay) {
setState(() => focusedDay = focused);
}
}
},
),
],
);
}
class CalendarHeader extends StatelessWidget {
const CalendarHeader({
super.key,
required this.focusedDay,
required this.onLeftChevronTap,
required this.onRightChevronTap,
required this.onTodayButtonTap,
this.iconColor,
this.titleStyle,
this.locale,
this.twoRowHeader = false,
});
final DateTime focusedDay;
final VoidCallback onLeftChevronTap;
final VoidCallback onRightChevronTap;
final VoidCallback onTodayButtonTap;
final Color? iconColor;
final TextStyle? titleStyle;
final String? locale;
final bool twoRowHeader;
@override
Widget build(BuildContext context) => Container(
decoration: const BoxDecoration(),
margin: const EdgeInsets.all(0),
padding: const EdgeInsets.symmetric(vertical: 8),
child: twoRowHeader ? _buildTwoRowHeader() : _buildOneRowHeader(),
);
Widget _buildTwoRowHeader() => Column(
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
const SizedBox(width: 16),
_buildDateWidget(),
],
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: _buildCustomIconButtons(),
),
],
);
Widget _buildOneRowHeader() => Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
const SizedBox(width: 16),
_buildDateWidget(),
..._buildCustomIconButtons(),
],
);
Widget _buildDateWidget() => Expanded(
child: Text(
DateFormat.yMMMM(locale).format(focusedDay),
style: const TextStyle(fontSize: 17).merge(titleStyle),
),
);
List<Widget> _buildCustomIconButtons() => <Widget>[
CustomIconButton(
icon: Icon(Icons.calendar_today, color: iconColor),
onTap: onTodayButtonTap,
),
CustomIconButton(
icon: Icon(Icons.chevron_left, color: iconColor),
onTap: onLeftChevronTap,
),
CustomIconButton(
icon: Icon(Icons.chevron_right, color: iconColor),
onTap: onRightChevronTap,
),
];
}
class CustomIconButton extends StatelessWidget {
const CustomIconButton({
super.key,
required this.icon,
required this.onTap,
this.margin = const EdgeInsets.symmetric(horizontal: 4),
this.padding = const EdgeInsets.all(10),
});
final Icon icon;
final VoidCallback onTap;
final EdgeInsetsGeometry margin;
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) => Padding(
padding: margin,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(100),
child: Padding(
padding: padding,
child: Icon(
icon.icon,
color: icon.color,
size: icon.size,
),
),
),
);
}
DateTime _previousWeek(DateTime week) {
return week.subtract(const Duration(days: 7));
}
DateTime _nextWeek(DateTime week) {
return week.add(const Duration(days: 7));
}
DateTime _previousMonth(DateTime month) {
if (month.month == 1) {
return DateTime(month.year - 1, 12);
} else {
return DateTime(month.year, month.month - 1);
}
}
DateTime _nextMonth(DateTime month) {
if (month.month == 12) {
return DateTime(month.year + 1, 1);
} else {
return DateTime(month.year, month.month + 1);
}
}

View File

@@ -0,0 +1,586 @@
import 'dart:math';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
export 'package:fl_chart/fl_chart.dart'
show BarAreaData, FlDotData, LineChartBarData, BarChartAlignment;
/// A line chart widget that displays a line chart with customizable data and styling.
///
/// The [FlutterFlowLineChart] widget is used to display a line chart in a Flutter application.
class FlutterFlowLineChart extends StatelessWidget {
const FlutterFlowLineChart({
super.key,
required this.data,
required this.xAxisLabelInfo,
required this.yAxisLabelInfo,
required this.axisBounds,
this.chartStylingInfo = const ChartStylingInfo(),
});
/// The data to be displayed in the line chart.
final List<FFLineChartData> data;
/// The information for labeling the x-axis.
final AxisLabelInfo xAxisLabelInfo;
/// The information for labeling the y-axis.
final AxisLabelInfo yAxisLabelInfo;
/// The bounds for the chart's axes.
final AxisBounds axisBounds;
/// The styling information for the chart.
final ChartStylingInfo chartStylingInfo;
List<LineChartBarData> get dataWithSpots =>
data.map((d) => d.settings.copyWith(spots: d.spots)).toList();
@override
Widget build(BuildContext context) => LineChart(
LineChartData(
lineTouchData: LineTouchData(
handleBuiltInTouches: chartStylingInfo.enableTooltip,
touchTooltipData: LineTouchTooltipData(
getTooltipColor: (group) =>
chartStylingInfo.tooltipBackgroundColor ?? Colors.black,
),
),
gridData: FlGridData(show: chartStylingInfo.showGrid),
borderData: FlBorderData(
border: Border.all(
color: chartStylingInfo.borderColor,
width: chartStylingInfo.borderWidth,
),
show: chartStylingInfo.showBorder,
),
titlesData: getTitlesData(
xAxisLabelInfo,
yAxisLabelInfo,
),
lineBarsData: dataWithSpots,
minX: axisBounds.minX,
minY: axisBounds.minY,
maxX: axisBounds.maxX,
maxY: axisBounds.maxY,
backgroundColor: chartStylingInfo.backgroundColor,
),
);
}
/// A bar chart widget that displays data in a bar format.
///
/// The [FlutterFlowBarChart] widget is used to create a bar chart in FlutterFlow.
class FlutterFlowBarChart extends StatelessWidget {
const FlutterFlowBarChart({
super.key,
required this.barData,
required this.xLabels,
required this.xAxisLabelInfo,
required this.yAxisLabelInfo,
required this.axisBounds,
this.stacked = false,
this.barWidth,
this.barBorderRadius,
this.barSpace,
this.groupSpace,
this.alignment = BarChartAlignment.center,
this.chartStylingInfo = const ChartStylingInfo(),
});
/// The data for the bar chart.
final List<FFBarChartData> barData;
/// The labels for the x-axis of the bar chart.
final List<String> xLabels;
/// The information about the labels for the x-axis.
final AxisLabelInfo xAxisLabelInfo;
/// The information about the labels for the y-axis.
final AxisLabelInfo yAxisLabelInfo;
/// The bounds for the x and y axes.
final AxisBounds axisBounds;
/// Determines whether the bars in the chart are stacked.
final bool stacked;
/// The width of each bar in the chart.
final double? barWidth;
/// The border radius of each bar in the chart.
final BorderRadius? barBorderRadius;
/// The space between each bar in the chart.
final double? barSpace;
/// The space between each group of bars in the chart.
final double? groupSpace;
/// The alignment of the bars within the chart.
final BarChartAlignment alignment;
/// The styling information for the chart.
final ChartStylingInfo chartStylingInfo;
Map<int, List<double>> get dataMap => xLabels.asMap().map((key, value) =>
MapEntry(key, barData.map((data) => data.data[key]).toList()));
List<BarChartGroupData> get groups => dataMap.entries.map((entry) {
final groupInt = entry.key;
final groupData = entry.value;
return BarChartGroupData(
x: groupInt,
barsSpace: barSpace,
barRods: groupData.asMap().entries.map((rod) {
final rodInt = rod.key;
final rodSettings = barData[rodInt];
final rodValue = rod.value;
return BarChartRodData(
toY: rodValue,
color: rodSettings.color,
width: barWidth,
borderRadius: barBorderRadius,
borderSide: BorderSide(
width: rodSettings.borderWidth,
color: rodSettings.borderColor,
),
);
}).toList());
}).toList();
List<BarChartGroupData> get stacks => dataMap.entries.map((entry) {
final groupInt = entry.key;
final stackData = entry.value;
return BarChartGroupData(
x: groupInt,
barsSpace: barSpace,
barRods: [
BarChartRodData(
toY: sum(stackData),
width: barWidth,
borderRadius: barBorderRadius,
rodStackItems: stackData.asMap().entries.map((stack) {
final stackInt = stack.key;
final stackSettings = barData[stackInt];
final start =
stackInt == 0 ? 0.0 : sum(stackData.sublist(0, stackInt));
return BarChartRodStackItem(
start,
start + stack.value,
stackSettings.color,
BorderSide(
width: stackSettings.borderWidth,
color: stackSettings.borderColor,
),
);
}).toList(),
)
],
);
}).toList();
double sum(List<double> list) => list.reduce((a, b) => a + b);
@override
Widget build(BuildContext context) {
return BarChart(
BarChartData(
barTouchData: BarTouchData(
handleBuiltInTouches: chartStylingInfo.enableTooltip,
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (group) =>
chartStylingInfo.tooltipBackgroundColor ?? Colors.black,
),
),
alignment: alignment,
gridData: FlGridData(show: chartStylingInfo.showGrid),
borderData: FlBorderData(
border: Border.all(
color: chartStylingInfo.borderColor,
width: chartStylingInfo.borderWidth,
),
show: chartStylingInfo.showBorder,
),
titlesData: getTitlesData(
xAxisLabelInfo,
yAxisLabelInfo,
getXTitlesWidget: (val, _) => Text(
xLabels[val.toInt()],
style: xAxisLabelInfo.labelTextStyle,
),
),
barGroups: stacked ? stacks : groups,
groupsSpace: groupSpace,
minY: axisBounds.minY,
maxY: axisBounds.maxY,
backgroundColor: chartStylingInfo.backgroundColor,
),
);
}
}
enum PieChartSectionLabelType {
none,
value,
percent,
}
/// A widget that displays a pie chart using the provided data.
class FlutterFlowPieChart extends StatelessWidget {
const FlutterFlowPieChart({
super.key,
required this.data,
this.donutHoleRadius = 0,
this.donutHoleColor = Colors.transparent,
this.sectionLabelType = PieChartSectionLabelType.none,
this.sectionLabelStyle,
this.labelFormatter = const LabelFormatter(),
});
final FFPieChartData data;
final double donutHoleRadius;
final Color donutHoleColor;
final PieChartSectionLabelType sectionLabelType;
final TextStyle? sectionLabelStyle;
final LabelFormatter labelFormatter;
double get sumOfValues => data.data.reduce((a, b) => a + b);
@override
Widget build(BuildContext context) => PieChart(
PieChartData(
centerSpaceRadius: donutHoleRadius,
centerSpaceColor: donutHoleColor,
sectionsSpace: 0,
sections: data.data.asMap().entries.map(
(section) {
String? title;
final index = section.key;
final sectionData = section.value;
final colorsLength = data.colors.length;
final otherPropsLength = data.radius.length;
switch (sectionLabelType) {
case PieChartSectionLabelType.value:
title = formatLabel(labelFormatter, sectionData);
break;
case PieChartSectionLabelType.percent:
title =
'\${formatLabel(labelFormatter, sectionData / sumOfValues * 100)}%';
break;
default:
break;
}
return PieChartSectionData(
value: sectionData,
color: data.colors[index % colorsLength],
radius: otherPropsLength == 1
? data.radius.first
: data.radius[index],
borderSide: BorderSide(
color: (otherPropsLength == 1
? data.borderColor?.first
: data.borderColor?.elementAt(index)) ??
Colors.transparent,
width: (otherPropsLength == 1
? data.borderWidth?.first
: data.borderWidth?.elementAt(index)) ??
0.0,
),
showTitle: sectionLabelType != PieChartSectionLabelType.none,
titleStyle: sectionLabelStyle,
title: title,
);
},
).toList(),
),
);
}
class FlutterFlowChartLegendWidget extends StatelessWidget {
const FlutterFlowChartLegendWidget({
super.key,
required this.entries,
this.width,
this.height,
this.textStyle,
this.padding,
this.backgroundColor = Colors.transparent,
this.borderRadius,
this.borderWidth = 1.0,
this.borderColor = const Color(0xFF000000),
this.indicatorSize = 10,
this.indicatorBorderRadius,
this.textPadding = const EdgeInsets.all(0),
});
final List<LegendEntry> entries;
final double? width;
final double? height;
final TextStyle? textStyle;
final EdgeInsetsGeometry? padding;
final Color backgroundColor;
final BorderRadius? borderRadius;
final double borderWidth;
final Color borderColor;
final double indicatorSize;
final BorderRadius? indicatorBorderRadius;
final EdgeInsetsGeometry textPadding;
@override
Widget build(BuildContext context) => Container(
width: width,
height: height,
padding: padding,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: borderRadius,
border: Border.all(
color: borderColor,
width: borderWidth,
),
),
child: Column(
children: entries
.map(
(entry) => Row(
children: [
Container(
height: indicatorSize,
width: indicatorSize,
decoration: BoxDecoration(
color: entry.color,
borderRadius: indicatorBorderRadius,
),
),
Padding(
padding: textPadding,
child: Text(
entry.name,
style: textStyle,
),
)
],
),
)
.toList(),
),
);
}
class LegendEntry {
const LegendEntry(this.color, this.name);
final Color color;
final String name;
}
class ChartStylingInfo {
const ChartStylingInfo({
this.backgroundColor = Colors.white,
this.showGrid = false,
this.enableTooltip = false,
this.tooltipBackgroundColor,
this.borderColor = Colors.black,
this.borderWidth = 1.0,
this.showBorder = true,
});
final Color backgroundColor;
final bool showGrid;
final bool enableTooltip;
final Color? tooltipBackgroundColor;
final Color borderColor;
final double borderWidth;
final bool showBorder;
}
class AxisLabelInfo {
const AxisLabelInfo({
this.title = '',
this.titleTextStyle,
this.showLabels = false,
this.labelTextStyle,
this.labelInterval,
this.labelFormatter = const LabelFormatter(),
this.reservedSize,
});
final String title;
final TextStyle? titleTextStyle;
final bool showLabels;
final TextStyle? labelTextStyle;
final double? labelInterval;
final LabelFormatter labelFormatter;
final double? reservedSize;
}
class LabelFormatter {
const LabelFormatter({
this.numberFormat,
});
final String Function(double)? numberFormat;
NumberFormat get defaultFormat => NumberFormat()..significantDigits = 2;
}
class AxisBounds {
const AxisBounds({this.minX, this.minY, this.maxX, this.maxY});
final double? minX;
final double? minY;
final double? maxX;
final double? maxY;
}
class FFLineChartData {
const FFLineChartData({
required this.xData,
required this.yData,
required this.settings,
});
final List<dynamic> xData;
final List<dynamic> yData;
final LineChartBarData settings;
List<FlSpot> get spots {
final x = _dataToDouble(xData);
final y = _dataToDouble(yData);
assert(x.length == y.length, 'X and Y data must be the same length');
return Iterable<int>.generate(min(x.length, y.length))
.where((i) => x[i] != null && y[i] != null)
.map((i) => FlSpot(x[i]!, y[i]!))
.toList();
}
}
class FFBarChartData {
const FFBarChartData({
required this.yData,
required this.color,
this.borderWidth = 0,
this.borderColor = Colors.transparent,
});
final List<dynamic> yData;
final Color color;
final double borderWidth;
final Color borderColor;
List<double> get data => _dataToDouble(yData).map((e) => e ?? 0.0).toList();
}
class FFPieChartData {
const FFPieChartData({
required this.values,
required this.colors,
required this.radius,
this.borderWidth,
this.borderColor,
});
final List<dynamic> values;
final List<Color> colors;
final List<double> radius;
final List<double>? borderWidth;
final List<Color>? borderColor;
List<double> get data => _dataToDouble(values).map((e) => e ?? 0.0).toList();
}
List<double?> _dataToDouble(List<dynamic> data) {
if (data.isEmpty) {
return [];
}
if (data.first is double) {
return data.map((d) => d as double).toList();
}
if (data.first is int) {
return data.map((d) => (d as int).toDouble()).toList();
}
if (data.first is DateTime) {
return data
.map((d) => (d as DateTime).millisecondsSinceEpoch.toDouble())
.toList();
}
if (data.first is String) {
// First try to parse as doubles
if (double.tryParse(data.first as String) != null) {
return data.map((d) => double.tryParse(d as String)).toList();
}
if (int.tryParse(data.first as String) != null) {
return data.map((d) => int.tryParse(d as String)?.toDouble()).toList();
}
if (DateTime.tryParse(data.first as String) != null) {
return data
.map((d) =>
DateTime.tryParse(d as String)?.millisecondsSinceEpoch.toDouble())
.toList();
}
}
return [];
}
FlTitlesData getTitlesData(
AxisLabelInfo xAxisLabelInfo,
AxisLabelInfo yAxisLabelInfo, {
Widget Function(double, TitleMeta)? getXTitlesWidget,
}) =>
FlTitlesData(
bottomTitles: AxisTitles(
axisNameWidget: xAxisLabelInfo.title.isEmpty
? null
: Text(
xAxisLabelInfo.title,
style: xAxisLabelInfo.titleTextStyle,
),
axisNameSize: xAxisLabelInfo.titleTextStyle?.fontSize != null
? xAxisLabelInfo.titleTextStyle!.fontSize! + 12
: 16,
sideTitles: SideTitles(
getTitlesWidget: (val, meta) => getXTitlesWidget != null
? getXTitlesWidget(val, meta)
: Text(
formatLabel(xAxisLabelInfo.labelFormatter, val),
style: xAxisLabelInfo.labelTextStyle,
),
showTitles: xAxisLabelInfo.showLabels,
interval: xAxisLabelInfo.labelInterval,
reservedSize: xAxisLabelInfo.reservedSize ?? 22,
),
),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
axisNameWidget: yAxisLabelInfo.title.isEmpty
? null
: Text(
yAxisLabelInfo.title,
style: yAxisLabelInfo.titleTextStyle,
),
axisNameSize: yAxisLabelInfo.titleTextStyle?.fontSize != null
? yAxisLabelInfo.titleTextStyle!.fontSize! + 12
: 16,
sideTitles: SideTitles(
getTitlesWidget: (val, _) => Text(
formatLabel(yAxisLabelInfo.labelFormatter, val),
style: yAxisLabelInfo.labelTextStyle,
),
showTitles: yAxisLabelInfo.showLabels,
interval: yAxisLabelInfo.labelInterval,
reservedSize: yAxisLabelInfo.reservedSize ?? 22,
),
),
);
String formatLabel(LabelFormatter formatter, double value) {
if (formatter.numberFormat != null) {
return formatter.numberFormat!(value);
}
return formatter.defaultFormat.format(value);
}

View File

@@ -0,0 +1,148 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutterflow_ui/src/utils/form_field_controller.dart';
/// A group of checkboxes that allows the user to select multiple options.
class FlutterFlowCheckboxGroup extends StatefulWidget {
/// Creates a [FlutterFlowCheckboxGroup].
///
/// - [options] parameter is a list of strings representing the available options.
/// - [onChanged] parameter is a callback function that is called when the selection changes.
/// - [controller] parameter is a controller for the form field that holds the selected options.
/// - [textStyle] parameter is the style of the text for the checkboxes.
/// - [labelPadding] parameter is the padding around the checkbox labels.
/// - [itemPadding] parameter is the padding around each checkbox item.
/// - [activeColor] parameter is the color of the checkbox when it is selected.
/// - [checkColor] parameter is the color of the check mark inside the checkbox.
/// - [checkboxBorderRadius] parameter is the border radius of the checkbox.
/// - [checkboxBorderColor] parameter is the color of the checkbox border.
/// - [initialized] parameter indicates whether the checkbox group is initialized with a value.
/// - [unselectedTextStyle] parameter is the style of the text for unselected checkboxes.
const FlutterFlowCheckboxGroup({
super.key,
required this.options,
required this.onChanged,
required this.controller,
required this.textStyle,
this.labelPadding,
this.itemPadding,
required this.activeColor,
required this.checkColor,
this.checkboxBorderRadius,
required this.checkboxBorderColor,
this.initialized = true,
this.unselectedTextStyle,
});
final List<String> options;
final void Function(List<String>)? onChanged;
final FormFieldController<List<String>> controller;
final TextStyle textStyle;
final EdgeInsetsGeometry? labelPadding;
final EdgeInsetsGeometry? itemPadding;
final Color activeColor;
final Color checkColor;
final BorderRadius? checkboxBorderRadius;
final Color checkboxBorderColor;
final bool initialized;
final TextStyle? unselectedTextStyle;
@override
State<FlutterFlowCheckboxGroup> createState() =>
_FlutterFlowCheckboxGroupState();
}
class _FlutterFlowCheckboxGroupState extends State<FlutterFlowCheckboxGroup> {
late List<String> checkboxValues;
late void Function() _selectedValueListener;
ValueListenable<List<String>?> get changeSelectedValues => widget.controller;
List<String> get selectedValues => widget.controller.value ?? [];
@override
void initState() {
super.initState();
checkboxValues = List.from(widget.controller.initialValue ?? []);
if (!widget.initialized && checkboxValues.isNotEmpty) {
SchedulerBinding.instance.addPostFrameCallback(
(_) {
if (widget.onChanged != null) {
widget.onChanged!(checkboxValues);
}
},
);
}
_selectedValueListener = () {
if (!listEquals(checkboxValues, selectedValues)) {
setState(() => checkboxValues = List.from(selectedValues));
}
if (widget.onChanged != null) {
widget.onChanged!(selectedValues);
}
};
changeSelectedValues.addListener(_selectedValueListener);
}
@override
void dispose() {
changeSelectedValues.removeListener(_selectedValueListener);
super.dispose();
}
@override
Widget build(BuildContext context) => ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: widget.options.length,
itemBuilder: (context, index) {
final option = widget.options[index];
final selected = selectedValues.contains(option);
final unselectedTextStyle =
widget.unselectedTextStyle ?? widget.textStyle;
return Theme(
data: ThemeData(unselectedWidgetColor: widget.checkboxBorderColor),
child: Padding(
padding: widget.itemPadding ?? EdgeInsets.zero,
child: Row(
children: [
Checkbox(
value: selected,
onChanged: widget.onChanged != null
? (isSelected) {
if (isSelected == null) {
return;
}
isSelected
? checkboxValues.add(option)
: checkboxValues.remove(option);
widget.controller.value = List.from(checkboxValues);
setState(() {});
}
: null,
activeColor: widget.activeColor,
checkColor: widget.checkColor,
shape: RoundedRectangleBorder(
borderRadius:
widget.checkboxBorderRadius ?? BorderRadius.zero,
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
),
Expanded(
child: Padding(
padding: widget.labelPadding ?? EdgeInsets.zero,
child: Text(
widget.options[index],
style:
selected ? widget.textStyle : unselectedTextStyle,
),
),
),
],
),
),
);
},
);
}

View File

@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutterflow_ui/src/utils/flutter_flow_helpers.dart';
import 'package:flutterflow_ui/src/utils/form_field_controller.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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 FlutterFlowChoiceChips extends StatefulWidget {
const FlutterFlowChoiceChips({
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<FlutterFlowChoiceChips> createState() => _FlutterFlowChoiceChipsState();
}
class _FlutterFlowChoiceChipsState extends State<FlutterFlowChoiceChips> {
late List<String> choiceChipValues;
List<String> get selectedValues => widget.controller.value ?? [];
@override
void initState() {
super.initState();
choiceChipValues = List.from(selectedValues);
if (!widget.initialized && choiceChipValues.isNotEmpty) {
SchedulerBinding.instance.addPostFrameCallback(
(_) {
if (widget.onChanged != null) {
widget.onChanged!(choiceChipValues);
}
},
);
}
}
@override
void dispose() {
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) {
choiceChipValues = List.from(selectedValues);
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(() {});
}
}
widget.onChanged!(choiceChipValues);
}
: 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 FlutterFlowCountController extends StatefulWidget {
const FlutterFlowCountController({
super.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),
});
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
State<FlutterFlowCountController> createState() =>
_FlutterFlowCountControllerState();
}
class _FlutterFlowCountControllerState
extends State<FlutterFlowCountController> {
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,305 @@
import 'package:flutter/material.dart';
import 'package:flutter_credit_card/flutter_credit_card.dart';
// ignore: implementation_imports
import 'package:flutter_credit_card/src/masked_text_controller.dart';
export 'package:flutter_credit_card/flutter_credit_card.dart'
show CreditCardModel;
/// Modified from https://pub.dev/packages/flutter_credit_card (see license below)
CreditCardModel emptyCreditCard() => CreditCardModel('', '', '', '', false);
/// A form widget for entering credit card information.
class FlutterFlowCreditCardForm extends StatefulWidget {
/// Creates a [FlutterFlowCreditCardForm].
///
/// - [formKey] is a global key that uniquely identifies the form.
/// - [creditCardModel] is the model that holds the credit card information.
/// - [obscureNumber] determines whether the credit card number should be obscured.
/// - [obscureCvv] determines whether the CVV should be obscured.
/// - [textStyle] is the style of the text in the form.
/// - [spacing] is the spacing between form fields.
/// - [inputDecoration] is the decoration for the form fields.
const FlutterFlowCreditCardForm({
super.key,
required this.formKey,
required this.creditCardModel,
this.obscureNumber = false,
this.obscureCvv = false,
this.textStyle,
this.spacing = 10.0,
this.inputDecoration = const InputDecoration(
border: OutlineInputBorder(),
),
});
final GlobalKey<FormState> formKey;
final CreditCardModel creditCardModel;
final bool obscureNumber;
final bool obscureCvv;
final TextStyle? textStyle;
final double spacing;
final InputDecoration inputDecoration;
@override
State<FlutterFlowCreditCardForm> createState() =>
_FlutterFlowCreditCardFormState();
}
class _FlutterFlowCreditCardFormState extends State<FlutterFlowCreditCardForm> {
final TextEditingController _cardNumberController =
MaskedTextController(mask: '0000 0000 0000 0000');
final TextEditingController _expiryDateController =
MaskedTextController(mask: '00/00');
final TextEditingController _cvvCodeController =
MaskedTextController(mask: '0000');
FocusNode cvvFocusNode = FocusNode();
FocusNode cardNumberNode = FocusNode();
FocusNode expiryDateNode = FocusNode();
String get cardNumber => widget.creditCardModel.cardNumber;
void textFieldFocusDidChange() {
widget.creditCardModel.isCvvFocused = cvvFocusNode.hasFocus;
}
@override
void initState() {
super.initState();
if (widget.creditCardModel.cardNumber.isNotEmpty) {
_cardNumberController.text = widget.creditCardModel.cardNumber;
}
if (widget.creditCardModel.expiryDate.isNotEmpty) {
_expiryDateController.text = widget.creditCardModel.expiryDate;
}
if (widget.creditCardModel.cvvCode.isNotEmpty) {
_cvvCodeController.text = widget.creditCardModel.cvvCode;
}
cvvFocusNode.addListener(textFieldFocusDidChange);
_cardNumberController.addListener(() => setState(
() => widget.creditCardModel.cardNumber = _cardNumberController.text));
_expiryDateController.addListener(() => setState(
() => widget.creditCardModel.expiryDate = _expiryDateController.text));
_cvvCodeController.addListener(() => setState(
() => widget.creditCardModel.cvvCode = _cvvCodeController.text));
}
@override
void dispose() {
cvvFocusNode.dispose();
expiryDateNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Form(
key: widget.formKey,
child: Column(
children: <Widget>[
Container(
margin: const EdgeInsets.only(top: 12.0),
child: TextFormField(
obscureText: widget.obscureNumber,
controller: _cardNumberController,
onEditingComplete: () =>
FocusScope.of(context).requestFocus(expiryDateNode),
style: widget.textStyle,
decoration: widget.inputDecoration.copyWith(
labelText: 'Card number',
hintText: 'XXXX XXXX XXXX XXXX',
labelStyle: widget.textStyle,
hintStyle: widget.textStyle,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
// Validate less that 13 digits +3 white spaces
if (value == null || value.isEmpty || value.length < 16) {
return 'Please input a valid number';
}
return null;
},
),
),
SizedBox(height: widget.spacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: Container(
margin: const EdgeInsets.only(top: 8.0, bottom: 8.0),
child: TextFormField(
controller: _expiryDateController,
focusNode: expiryDateNode,
onEditingComplete: () {
FocusScope.of(context).requestFocus(cvvFocusNode);
},
style: widget.textStyle,
decoration: widget.inputDecoration.copyWith(
labelText: 'Exp. Date',
hintText: 'MM/YY',
labelStyle: widget.textStyle,
hintStyle: widget.textStyle,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please input a valid date';
}
final DateTime now = DateTime.now();
final List<String> date = value.split(RegExp(r'/'));
final int month = int.parse(date.first);
final int year = int.parse('20\${date.last}');
final DateTime cardDate = DateTime(year, month);
if (cardDate.isBefore(now) ||
month > 12 ||
month == 0) {
return 'Please input a valid date';
}
return null;
},
),
),
),
const SizedBox(width: 16.0),
Expanded(
child: Container(
margin: const EdgeInsets.only(top: 8.0, bottom: 8.0),
child: TextFormField(
obscureText: widget.obscureCvv,
focusNode: cvvFocusNode,
controller: _cvvCodeController,
style: widget.textStyle,
decoration: widget.inputDecoration.copyWith(
labelText: 'CVV',
hintText: 'XXX',
labelStyle: widget.textStyle,
hintStyle: widget.textStyle,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null ||
value.isEmpty ||
value.length < 3) {
return 'Please input a valid CVV';
}
return null;
},
),
),
),
],
),
],
),
);
/// Credit Card prefix patterns as of March 2019
/// A [List<String>] represents a range.
/// i.e. ['51', '55'] represents the range of cards starting with '51' to those starting with '55'
Map<CardType, Set<List<String>>> cardNumPatterns =
<CardType, Set<List<String>>>{
CardType.visa: <List<String>>{
<String>['4'],
},
CardType.americanExpress: <List<String>>{
<String>['34'],
<String>['37'],
},
CardType.discover: <List<String>>{
<String>['6011'],
<String>['622126', '622925'],
<String>['644', '649'],
<String>['65']
},
CardType.mastercard: <List<String>>{
<String>['51', '55'],
<String>['2221', '2229'],
<String>['223', '229'],
<String>['23', '26'],
<String>['270', '271'],
<String>['2720'],
},
};
/// This function determines the Credit Card type based on the cardPatterns
/// and returns it.
CardType detectCCType(String cardNumber) {
//Default card type is other
CardType cardType = CardType.otherBrand;
if (cardNumber.isEmpty) {
return cardType;
}
cardNumPatterns.forEach(
(type, patterns) {
for (final patternRange in patterns) {
// Remove any spaces
String ccPatternStr = cardNumber.replaceAll(RegExp(r's+\b|\bs'), '');
final int rangeLen = patternRange[0].length;
// Trim the Credit Card number string to match the pattern prefix length
if (rangeLen < cardNumber.length) {
ccPatternStr = ccPatternStr.substring(0, rangeLen);
}
if (patternRange.length > 1) {
// Convert the prefix range into numbers then make sure the
// Credit Card num is in the pattern range.
// Because Strings don't have '>=' type operators
final int ccPrefixAsInt = int.parse(ccPatternStr);
final int startPatternPrefixAsInt = int.parse(patternRange[0]);
final int endPatternPrefixAsInt = int.parse(patternRange[1]);
if (ccPrefixAsInt >= startPatternPrefixAsInt &&
ccPrefixAsInt <= endPatternPrefixAsInt) {
// Found a match
cardType = type;
break;
}
} else {
// Just compare the single pattern prefix with the Credit Card prefix
if (ccPatternStr == patternRange[0]) {
// Found a match
cardType = type;
break;
}
}
}
},
);
return cardType;
}
}
/// BSD 2-Clause License
/// Copyright (c) 2019, Simform Solutions
/// All rights reserved.
/// 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.

View File

@@ -0,0 +1,342 @@
import 'dart:math' as math;
import 'package:data_table_2/data_table_2.dart';
import 'package:flutter/material.dart';
export 'package:data_table_2/data_table_2.dart' show DataColumn2;
const _kDataTableHorizontalMargin = 48.0;
const kDefaultColumnSpacing = 56.0;
const _kMinRowsPerPage = 5;
typedef ColumnsBuilder<T> = List<DataColumn> Function(void Function(int, bool));
typedef DataRowBuilder<T> = DataRow? Function(
T, int, bool, void Function(bool?)?);
class FlutterFlowDataTableController<T> extends DataTableSource {
FlutterFlowDataTableController({
List<T>? initialData,
int? numRows,
PaginatorController? paginatorController,
bool selectable = false,
}) {
data = initialData?.toList() ?? [];
numRows = numRows;
this.paginatorController = paginatorController ?? PaginatorController();
_selectable = selectable;
}
DataRowBuilder<T>? _dataRowBuilder;
late PaginatorController paginatorController;
List<T> data = [];
int? _numRows;
List<T> get selectedData =>
selectedRows.where((i) => i < data.length).map(data.elementAt).toList();
bool _selectable = false;
final Set<int> selectedRows = {};
int rowsPerPage = defaultRowsPerPage;
int? sortColumnIndex;
bool sortAscending = true;
void init({
DataRowBuilder<T>? dataRowBuilder,
bool? selectable,
List<T>? initialData,
int? initialNumRows,
}) {
_dataRowBuilder = dataRowBuilder ?? _dataRowBuilder;
_selectable = selectable ?? _selectable;
data = initialData?.toList() ?? data;
_numRows = initialNumRows;
}
void updateData({
List<T>? data,
int? numRows,
bool notify = true,
}) {
this.data = data?.toList() ?? this.data;
_numRows = numRows ?? _numRows;
if (notify) {
notifyListeners();
}
}
void updateSort({
required int columnIndex,
required bool ascending,
Function(int, bool)? onSortChanged,
}) {
sortColumnIndex = columnIndex;
sortAscending = ascending;
if (onSortChanged != null) {
onSortChanged(columnIndex, ascending);
}
notifyListeners();
}
@override
DataRow? getRow(int index) {
final row = data.elementAtOrNull(index);
return _dataRowBuilder != null && row != null
? _dataRowBuilder!(
row,
index,
selectedRows.contains(index),
_selectable
? (selected) {
if (selected == null) {
return;
}
selected
? selectedRows.add(index)
: selectedRows.remove(index);
notifyListeners();
}
: null,
)
: null;
}
@override
bool get isRowCountApproximate => false;
@override
int get rowCount => _numRows ?? data.length;
@override
int get selectedRowCount => selectedRows.length;
}
/// A widget that displays tabular data in a grid format.
class FlutterFlowDataTable<T> extends StatefulWidget {
const FlutterFlowDataTable({
super.key,
required this.controller,
required this.data,
this.numRows,
required this.columnsBuilder,
required this.dataRowBuilder,
this.emptyBuilder,
this.onPageChanged,
this.onSortChanged,
this.onRowsPerPageChanged,
this.paginated = true,
this.selectable = false,
this.hidePaginator = false,
this.showFirstLastButtons = false,
this.width,
this.height,
this.minWidth,
this.headingRowHeight = 56,
this.dataRowHeight = kMinInteractiveDimension,
this.columnSpacing = kDefaultColumnSpacing,
this.headingRowColor,
this.sortIconColor,
this.borderRadius,
this.addHorizontalDivider = true,
this.addTopAndBottomDivider = false,
this.hideDefaultHorizontalDivider = false,
this.addVerticalDivider = false,
this.horizontalDividerColor,
this.horizontalDividerThickness,
this.verticalDividerColor,
this.verticalDividerThickness,
this.checkboxUnselectedFillColor,
this.checkboxSelectedFillColor,
this.checkboxUnselectedBorderColor,
this.checkboxSelectedBorderColor,
this.checkboxCheckColor,
});
final FlutterFlowDataTableController<T> controller;
final List<T> data;
final int? numRows;
final ColumnsBuilder columnsBuilder;
final DataRowBuilder<T> dataRowBuilder;
final Widget? Function()? emptyBuilder;
// Callback functions
final Function(int)? onPageChanged;
final Function(int, bool)? onSortChanged;
final Function(int)? onRowsPerPageChanged;
// Functionality options
final bool paginated;
final bool selectable;
final bool showFirstLastButtons;
final bool hidePaginator;
// Size and shape options
final double? width;
final double? height;
final double? minWidth;
final double headingRowHeight;
final double dataRowHeight;
final double columnSpacing;
// Table style options
final Color? headingRowColor;
final Color? sortIconColor;
final BorderRadius? borderRadius;
final bool addHorizontalDivider;
final bool addTopAndBottomDivider;
final bool hideDefaultHorizontalDivider;
final Color? horizontalDividerColor;
final double? horizontalDividerThickness;
final bool addVerticalDivider;
final Color? verticalDividerColor;
final double? verticalDividerThickness;
// Checkbox style options
final Color? checkboxUnselectedFillColor;
final Color? checkboxSelectedFillColor;
final Color? checkboxUnselectedBorderColor;
final Color? checkboxSelectedBorderColor;
final Color? checkboxCheckColor;
@override
State<FlutterFlowDataTable<T>> createState() =>
_FlutterFlowDataTableState<T>();
}
class _FlutterFlowDataTableState<T> extends State<FlutterFlowDataTable<T>> {
FlutterFlowDataTableController<T> get controller => widget.controller;
int get rowCount => controller.rowCount;
int get initialRowsPerPage =>
rowCount > _kMinRowsPerPage ? defaultRowsPerPage : _kMinRowsPerPage;
@override
void initState() {
super.initState();
dataTableShowLogs = false; // Disables noisy DataTable2 debug statements.
controller.init(
dataRowBuilder: widget.dataRowBuilder,
selectable: widget.selectable,
initialData: widget.data,
initialNumRows: widget.numRows,
);
// ignore: cascade_invocations
controller.addListener(() => setState(() {}));
}
@override
void didUpdateWidget(FlutterFlowDataTable<T> oldWidget) {
super.didUpdateWidget(oldWidget);
controller.updateData(
data: widget.data,
numRows: widget.numRows,
notify: true,
);
}
@override
Widget build(BuildContext context) {
final columns = widget.columnsBuilder(
(index, ascending) {
controller.updateSort(
columnIndex: index,
ascending: ascending,
onSortChanged: widget.onSortChanged,
);
setState(() {});
},
);
final checkboxThemeData = CheckboxThemeData(
checkColor: WidgetStateProperty.all(
widget.checkboxCheckColor ?? Colors.black54,
),
fillColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.selected)
? widget.checkboxSelectedFillColor ?? Colors.white.withOpacity(0.01)
: widget.checkboxUnselectedFillColor ??
Colors.white.withOpacity(0.01),
),
side: WidgetStateBorderSide.resolveWith(
(states) => BorderSide(
width: 2.0,
color: states.contains(WidgetState.selected)
? widget.checkboxSelectedBorderColor ?? Colors.black54
: widget.checkboxUnselectedBorderColor ?? Colors.black54,
),
),
overlayColor: WidgetStateProperty.all(Colors.transparent),
);
final horizontalBorder = widget.addHorizontalDivider
? BorderSide(
color: widget.horizontalDividerColor ?? Colors.transparent,
width: widget.horizontalDividerThickness ?? 1.0,
)
: BorderSide.none;
return ClipRRect(
borderRadius: widget.borderRadius ?? BorderRadius.zero,
child: SizedBox(
width: widget.width,
height: widget.height,
child: Theme(
data: Theme.of(context).copyWith(
iconTheme: widget.sortIconColor != null
? IconThemeData(color: widget.sortIconColor)
: null,
),
child: PaginatedDataTable2(
source: controller,
controller:
widget.paginated ? controller.paginatorController : null,
rowsPerPage: widget.paginated ? initialRowsPerPage : rowCount,
availableRowsPerPage: const [5, 10, 25, 50, 100],
onPageChanged: widget.onPageChanged != null
? (index) => widget.onPageChanged!(index)
: null,
columnSpacing: widget.columnSpacing,
onRowsPerPageChanged: widget.paginated
? (value) {
controller.rowsPerPage = value ?? initialRowsPerPage;
if (widget.onRowsPerPageChanged != null) {
widget.onRowsPerPageChanged!(controller.rowsPerPage);
}
}
: null,
columns: columns,
empty: widget.emptyBuilder != null ? widget.emptyBuilder!() : null,
sortColumnIndex: controller.sortColumnIndex,
sortAscending: controller.sortAscending,
showCheckboxColumn: widget.selectable,
datarowCheckboxTheme: checkboxThemeData,
headingCheckboxTheme: checkboxThemeData,
hidePaginator: !widget.paginated || widget.hidePaginator,
wrapInCard: false,
renderEmptyRowsInTheEnd: false,
border: TableBorder(
horizontalInside: horizontalBorder,
verticalInside: widget.addVerticalDivider
? BorderSide(
color: widget.verticalDividerColor ?? Colors.transparent,
width: widget.verticalDividerThickness ?? 1.0,
)
: BorderSide.none,
bottom: widget.addTopAndBottomDivider
? horizontalBorder
: BorderSide.none,
),
dividerThickness: widget.hideDefaultHorizontalDivider ? 0.0 : null,
headingRowColor: WidgetStateProperty.all(widget.headingRowColor),
headingRowHeight: widget.headingRowHeight,
dataRowHeight: widget.dataRowHeight,
showFirstLastButtons: widget.showFirstLastButtons,
minWidth: math.max(widget.minWidth ?? 0, _getColumnsWidth(columns)),
),
),
),
);
}
// Return the total fixed width of all columns that have a specified width,
// plus one to make the data table scrollable if there is insufficient space.
double _getColumnsWidth(List<DataColumn> columns) =>
columns.where((c) => c is DataColumn2 && c.fixedWidth != null).fold(
((widget.selectable ? 2 : 1) * _kDataTableHorizontalMargin) + 1,
(sum, col) => sum + (col as DataColumn2).fixedWidth!,
);
}

View File

@@ -0,0 +1,433 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import '../utils/form_field_controller.dart';
/// A dropdown widget that allows the user to select an option from a list of options.
class FlutterFlowDropDown<T> extends StatefulWidget {
const FlutterFlowDropDown({
super.key,
this.controller,
this.multiSelectController,
this.hintText,
this.searchHintText,
required this.options,
this.optionLabels,
this.onChanged,
this.onMultiSelectChanged,
this.icon,
this.width,
this.height,
this.maxHeight,
this.fillColor,
this.searchHintTextStyle,
this.searchTextStyle,
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.menuOffset,
this.isSearchable = false,
this.isMultiSelect = false,
this.labelText,
this.labelTextStyle,
}) : assert(
isMultiSelect
? (controller == null &&
onChanged == null &&
multiSelectController != null &&
onMultiSelectChanged != null)
: (controller != null &&
onChanged != null &&
multiSelectController == null &&
onMultiSelectChanged == null),
);
/// The controller for the dropdown field.
final FormFieldController<T?>? controller;
/// The controller for the multi-select dropdown field.
final FormFieldController<List<T>?>? multiSelectController;
/// The text to display as a hint when no option is selected.
final String? hintText;
/// The text to display as a hint in the search field.
final String? searchHintText;
/// The list of options to display in the dropdown.
final List<T> options;
/// The list of labels corresponding to the options.
final List<String>? optionLabels;
/// A callback function that is called when the selected option changes.
final Function(T?)? onChanged;
/// A callback function that is called when the selected options change in multi-select mode.
final Function(List<T>?)? onMultiSelectChanged;
/// The icon to display in the dropdown field.
final Widget? icon;
/// The width of the dropdown field.
final double? width;
/// The height of the dropdown field.
final double? height;
/// The maximum height of the dropdown menu.
final double? maxHeight;
/// The background color of the dropdown field.
final Color? fillColor;
/// The text style for the search hint text.
final TextStyle? searchHintTextStyle;
/// The text style for the search text.
final TextStyle? searchTextStyle;
/// The color of the search cursor.
final Color? searchCursorColor;
/// The text style for the dropdown field.
final TextStyle textStyle;
/// The elevation of the dropdown menu.
final double elevation;
/// The width of the dropdown field's border.
final double borderWidth;
/// The border radius of the dropdown field.
final double borderRadius;
/// The color of the dropdown field's border.
final Color borderColor;
/// The margin around the dropdown field.
final EdgeInsetsGeometry margin;
/// Whether to hide the underline of the dropdown field.
final bool hidesUnderline;
/// Whether the dropdown is disabled.
final bool disabled;
/// Whether the dropdown menu is displayed over the button.
final bool isOverButton;
/// The offset of the dropdown menu.
final Offset? menuOffset;
/// Whether the dropdown is searchable.
final bool isSearchable;
/// Whether the dropdown is in multi-select mode.
final bool isMultiSelect;
/// The label text for the dropdown field.
final String? labelText;
/// The text style for the label text.
final TextStyle? labelTextStyle;
@override
State<FlutterFlowDropDown<T>> createState() => _FlutterFlowDropDownState<T>();
}
class _FlutterFlowDropDownState<T> extends State<FlutterFlowDropDown<T>> {
bool get isMultiSelect => widget.isMultiSelect;
FormFieldController<T?> get controller => widget.controller!;
FormFieldController<List<T>?> get multiSelectController =>
widget.multiSelectController!;
T? get currentValue {
final value = isMultiSelect
? multiSelectController.value?.firstOrNull
: controller.value;
return widget.options.contains(value) ? value : null;
}
Set<T> get currentValues {
if (!isMultiSelect || multiSelectController.value == null) {
return {};
}
return widget.options
.toSet()
.intersection(multiSelectController.value!.toSet());
}
Map<T, String> get optionLabels => 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],
),
),
);
EdgeInsetsGeometry get horizontalMargin => widget.margin.clamp(
EdgeInsetsDirectional.zero,
const EdgeInsetsDirectional.symmetric(horizontal: double.infinity),
);
late void Function() _listener;
final TextEditingController _textEditingController = TextEditingController();
@override
void initState() {
super.initState();
if (isMultiSelect) {
_listener =
() => widget.onMultiSelectChanged!(multiSelectController.value);
multiSelectController.addListener(_listener);
} else {
_listener = () => widget.onChanged!(controller.value);
controller.addListener(_listener);
}
}
@override
void dispose() {
if (isMultiSelect) {
multiSelectController.removeListener(_listener);
} else {
controller.removeListener(_listener);
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final dropdownWidget = _buildDropdownWidget();
return SizedBox(
width: widget.width,
height: widget.height,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.borderRadius),
border: Border.all(
color: widget.borderColor,
width: widget.borderWidth,
),
color: widget.fillColor,
),
child: Padding(
padding: _useDropdown2() ? EdgeInsets.zero : widget.margin,
child: widget.hidesUnderline
? DropdownButtonHideUnderline(child: dropdownWidget)
: dropdownWidget,
),
),
);
}
bool _useDropdown2() =>
widget.isMultiSelect ||
widget.isSearchable ||
!widget.isOverButton ||
widget.maxHeight != null;
Widget _buildDropdownWidget() =>
_useDropdown2() ? _buildDropdown() : _buildLegacyDropdown();
Widget _buildLegacyDropdown() {
return DropdownButtonFormField<T>(
value: currentValue,
hint: _createHintText(),
items: _createMenuItems(),
elevation: widget.elevation.toInt(),
onChanged: widget.disabled ? null : (value) => controller.value = value,
icon: widget.icon,
isExpanded: true,
dropdownColor: widget.fillColor,
focusColor: Colors.transparent,
decoration: InputDecoration(
labelText: widget.labelText == null || widget.labelText!.isEmpty
? null
: widget.labelText,
labelStyle: widget.labelTextStyle,
border: widget.hidesUnderline
? InputBorder.none
: const UnderlineInputBorder(),
),
);
}
Text? _createHintText() => widget.hintText != null
? Text(widget.hintText!, style: widget.textStyle)
: null;
List<DropdownMenuItem<T>> _createMenuItems() => widget.options
.map(
(option) => DropdownMenuItem<T>(
value: option,
child: Padding(
padding: _useDropdown2() ? horizontalMargin : EdgeInsets.zero,
child: Text(optionLabels[option] ?? '', style: widget.textStyle),
),
),
)
.toList();
List<DropdownMenuItem<T>> _createMultiselectMenuItems() => widget.options
.map(
(item) => DropdownMenuItem<T>(
value: item,
// Disable default onTap to avoid closing menu when selecting an item
enabled: false,
child: StatefulBuilder(
builder: (context, menuSetState) {
final isSelected =
multiSelectController.value?.contains(item) ?? false;
return InkWell(
onTap: () {
multiSelectController.value ??= [];
isSelected
? multiSelectController.value!.remove(item)
: multiSelectController.value!.add(item);
multiSelectController.update();
// 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: horizontalMargin,
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(
optionLabels[item]!,
style: widget.textStyle,
),
),
],
),
),
);
},
),
),
)
.toList();
Widget _buildDropdown() {
final overlayColor = WidgetStateProperty.resolveWith<Color?>((states) =>
states.contains(WidgetState.focused) ? Colors.transparent : null);
final iconStyleData = widget.icon != null
? IconStyleData(icon: widget.icon!)
: const IconStyleData();
return DropdownButton2<T>(
value: currentValue,
hint: _createHintText(),
items: isMultiSelect ? _createMultiselectMenuItems() : _createMenuItems(),
iconStyleData: iconStyleData,
buttonStyleData: ButtonStyleData(
elevation: widget.elevation.toInt(),
overlayColor: overlayColor,
padding: widget.margin,
),
menuItemStyleData: MenuItemStyleData(
overlayColor: overlayColor,
padding: EdgeInsets.zero,
),
dropdownStyleData: DropdownStyleData(
elevation: widget.elevation.toInt(),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
color: widget.fillColor,
),
isOverButton: widget.isOverButton,
offset: widget.menuOffset ?? Offset.zero,
maxHeight: widget.maxHeight,
padding: EdgeInsets.zero,
),
onChanged: widget.disabled
? null
: (isMultiSelect ? (_) {} : (val) => widget.controller!.value = val),
isExpanded: true,
selectedItemBuilder: (context) => widget.options
.map(
(item) => Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
isMultiSelect
? currentValues
.where((v) => optionLabels.containsKey(v))
.map((v) => optionLabels[v])
.join(', ')
: optionLabels[item]!,
style: widget.textStyle,
maxLines: 1,
),
),
)
.toList(),
dropdownSearchData: widget.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,
style: widget.searchTextStyle,
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 is to clear the search value when you close the menu
onMenuStateChange: widget.isSearchable
? (isOpen) {
if (!isOpen) {
_textEditingController.clear();
}
}
: null,
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
/// A widget that displays an expanded image view.
class FlutterFlowExpandedImageView extends StatelessWidget {
/// Creates a [FlutterFlowExpandedImageView].
///
/// - [image] parameter is required and represents the image to be displayed.
/// - [allowRotation] parameter determines whether rotation is allowed for the image.
/// - [useHeroAnimation] parameter determines whether to use a hero animation when transitioning to the expanded image view.
/// - [tag] parameter is an optional tag used for the hero animation.
const FlutterFlowExpandedImageView({
super.key,
required this.image,
this.allowRotation = false,
this.useHeroAnimation = true,
this.tag,
});
final Widget image;
final bool allowRotation;
final bool useHeroAnimation;
final Object? tag;
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.sizeOf(context);
return Material(
color: Colors.black,
child: SafeArea(
child: Stack(
children: [
SizedBox(
height: screenSize.height,
width: screenSize.width,
child: PhotoView.customChild(
minScale: 1.0,
maxScale: 3.0,
enableRotation: allowRotation,
heroAttributes: useHeroAnimation
? PhotoViewHeroAttributes(tag: tag!)
: null,
onScaleEnd: (context, details, value) {
if (value.scale! < 0.3) {
Navigator.pop(context);
}
},
child: image,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: IconButton(
color: Colors.black,
onPressed: () => Navigator.pop(context),
icon: const Icon(
Icons.close,
size: 32,
color: Colors.white,
),
),
)
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,301 @@
// import 'dart:async';
// import 'dart:math';
// import 'dart:ui';
// import 'package:cached_network_image/cached_network_image.dart';
// import 'package:flutter/foundation.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter/scheduler.dart';
// import 'package:flutterflow_ui/src/utils/lat_lng.dart' as latlng;
// import 'package:google_maps_flutter/google_maps_flutter.dart';
// export 'dart:async' show Completer;
// export 'package:flutterflow_ui/src/utils/lat_lng.dart' show LatLng;
// export 'package:google_maps_flutter/google_maps_flutter.dart' hide LatLng;
// enum GoogleMapStyle {
// standard,
// silver,
// retro,
// dark,
// night,
// aubergine,
// }
// enum GoogleMarkerColor {
// red,
// orange,
// yellow,
// green,
// cyan,
// azure,
// blue,
// violet,
// magenta,
// rose,
// }
// @immutable
// class MarkerImage {
// const MarkerImage({
// required this.imagePath,
// required this.isAssetImage,
// this.size = 20.0,
// });
// final String imagePath;
// final bool isAssetImage;
// final double size;
// @override
// bool operator ==(Object other) =>
// identical(this, other) ||
// (other is MarkerImage &&
// imagePath == other.imagePath &&
// isAssetImage == other.isAssetImage &&
// size == other.size);
// @override
// int get hashCode => Object.hash(imagePath, isAssetImage, size);
// }
// class FlutterFlowMarker {
// const FlutterFlowMarker(this.markerId, this.location, [this.onTap]);
// final String markerId;
// final latlng.LatLng location;
// final Future Function()? onTap;
// }
// /// A widget that displays a Google Map.
// class FlutterFlowGoogleMap extends StatefulWidget {
// /// Creates a [FlutterFlowGoogleMap] widget.
// const FlutterFlowGoogleMap({
// required this.controller,
// this.onCameraIdle,
// this.initialLocation,
// this.markers = const [],
// this.markerColor = GoogleMarkerColor.red,
// this.markerImage,
// this.mapType = MapType.normal,
// this.style = GoogleMapStyle.standard,
// this.initialZoom = 12,
// this.allowInteraction = true,
// this.allowZoom = true,
// this.showZoomControls = true,
// this.showLocation = true,
// this.showCompass = false,
// this.showMapToolbar = false,
// this.showTraffic = false,
// this.centerMapOnMarkerTap = false,
// super.key,
// });
// /// A [Completer] that completes with a [GoogleMapController] instance.
// final Completer<GoogleMapController> controller;
// /// An optional callback function that will be called when the camera movement
// /// has ended and the map is idle.
// final Function(latlng.LatLng)? onCameraIdle;
// /// The initial location to center the map on.
// final latlng.LatLng? initialLocation;
// /// An iterable of [FlutterFlowMarker] objects that represent markers to be
// /// displayed on the map.
// final Iterable<FlutterFlowMarker> markers;
// /// The color of the markers.
// final GoogleMarkerColor markerColor;
// /// A custom image to be used as the marker icon.
// final MarkerImage? markerImage;
// /// The type of map to be displayed.
// final MapType mapType;
// /// The style of the map.
// final GoogleMapStyle style;
// /// The initial zoom level of the map.
// final double initialZoom;
// /// Determines whether the user can interact with the map.
// final bool allowInteraction;
// /// Determines whether the user can zoom in and out of the map.
// final bool allowZoom;
// /// Determines whether to show zoom controls on the map.
// final bool showZoomControls;
// /// Determines whether to show the user's current location on the map.
// final bool showLocation;
// /// Determines whether to show a compass on the map.
// final bool showCompass;
// /// Determines whether to show a toolbar with map-related actions.
// final bool showMapToolbar;
// /// Determines whether to show traffic data on the map.
// final bool showTraffic;
// /// Determines whether to center the map on the tapped marker.
// final bool centerMapOnMarkerTap;
// @override
// State<StatefulWidget> createState() => _FlutterFlowGoogleMapState();
// }
// class _FlutterFlowGoogleMapState extends State<FlutterFlowGoogleMap> {
// double get initialZoom => max(double.minPositive, widget.initialZoom);
// LatLng get initialPosition =>
// widget.initialLocation?.toGoogleMaps() ?? const LatLng(0.0, 0.0);
// late Completer<GoogleMapController> _controller;
// BitmapDescriptor? _markerDescriptor;
// late LatLng currentMapCenter;
// void initializeMarkerBitmap() {
// final markerImage = widget.markerImage;
// if (markerImage == null) {
// _markerDescriptor = BitmapDescriptor.defaultMarkerWithHue(
// googleMarkerColorMap[widget.markerColor]!,
// );
// return;
// }
// SchedulerBinding.instance.addPostFrameCallback((_) {
// final markerImageSize = Size.square(markerImage.size);
// var imageProvider = markerImage.isAssetImage
// ? Image.asset(markerImage.imagePath).image
// : CachedNetworkImageProvider(markerImage.imagePath);
// if (!kIsWeb) {
// // workaround for https://github.com/flutter/flutter/issues/34657 to
// // enable marker resizing on Android and iOS.
// final targetHeight =
// (markerImage.size * MediaQuery.of(context).devicePixelRatio)
// .toInt();
// imageProvider = ResizeImage(
// imageProvider,
// height: targetHeight,
// policy: ResizeImagePolicy.fit,
// allowUpscaling: true,
// );
// }
// final imageConfiguration =
// createLocalImageConfiguration(context, size: markerImageSize);
// imageProvider
// .resolve(imageConfiguration)
// .addListener(ImageStreamListener((img, _) async {
// final bytes = await img.image.toByteData(format: ImageByteFormat.png);
// if (bytes != null && mounted) {
// _markerDescriptor = BitmapDescriptor.fromBytes(
// bytes.buffer.asUint8List(),
// size: markerImageSize,
// );
// setState(() {});
// }
// }));
// });
// }
// void onCameraIdle() => widget.onCameraIdle?.call(currentMapCenter.toLatLng());
// @override
// void initState() {
// super.initState();
// currentMapCenter = initialPosition;
// _controller = widget.controller;
// initializeMarkerBitmap();
// }
// @override
// void didUpdateWidget(FlutterFlowGoogleMap oldWidget) {
// super.didUpdateWidget(oldWidget);
// // Rebuild the marker bitmap if the marker image changes.
// if (widget.markerImage != oldWidget.markerImage) {
// initializeMarkerBitmap();
// setState(() {});
// }
// }
// @override
// Widget build(BuildContext context) => AbsorbPointer(
// absorbing: !widget.allowInteraction,
// child: GoogleMap(
// onMapCreated: (controller) async {
// _controller.complete(controller);
// await controller.setMapStyle(googleMapStyleStrings[widget.style]);
// },
// onCameraIdle: onCameraIdle,
// onCameraMove: (position) => currentMapCenter = position.target,
// initialCameraPosition: CameraPosition(
// target: initialPosition,
// zoom: initialZoom,
// ),
// mapType: widget.mapType,
// zoomGesturesEnabled: widget.allowZoom,
// zoomControlsEnabled: widget.showZoomControls,
// myLocationEnabled: widget.showLocation,
// compassEnabled: widget.showCompass,
// mapToolbarEnabled: widget.showMapToolbar,
// trafficEnabled: widget.showTraffic,
// markers: widget.markers
// .map(
// (m) => Marker(
// markerId: MarkerId(m.markerId),
// position: m.location.toGoogleMaps(),
// icon: _markerDescriptor ?? BitmapDescriptor.defaultMarker,
// onTap: () async {
// if (widget.centerMapOnMarkerTap) {
// final controller = await _controller.future;
// await controller.animateCamera(
// CameraUpdate.newLatLng(m.location.toGoogleMaps()),
// );
// currentMapCenter = m.location.toGoogleMaps();
// onCameraIdle();
// }
// await m.onTap?.call();
// },
// ),
// )
// .toSet(),
// ));
// }
// extension ToGoogleMapsLatLng on latlng.LatLng {
// LatLng toGoogleMaps() => LatLng(latitude, longitude);
// }
// extension GoogleMapsToLatLng on LatLng {
// latlng.LatLng toLatLng() => latlng.LatLng(latitude, longitude);
// }
// Map<GoogleMapStyle, String> googleMapStyleStrings = {
// GoogleMapStyle.standard: '[]',
// GoogleMapStyle.silver:
// r'[{"elementType":"geometry","stylers":[{"color":"#f5f5f5"}]},{"elementType":"labels.icon","stylers":[{"visibility":"off"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#f5f5f5"}]},{"featureType":"administrative.land_parcel","elementType":"labels.text.fill","stylers":[{"color":"#bdbdbd"}]},{"featureType":"poi","elementType":"geometry","stylers":[{"color":"#eeeeee"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#e5e5e5"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#ffffff"}]},{"featureType":"road.arterial","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#dadada"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"featureType":"road.local","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]},{"featureType":"transit.line","elementType":"geometry","stylers":[{"color":"#e5e5e5"}]},{"featureType":"transit.station","elementType":"geometry","stylers":[{"color":"#eeeeee"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#c9c9c9"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]}]',
// GoogleMapStyle.retro:
// r'[{"elementType":"geometry","stylers":[{"color":"#ebe3cd"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#523735"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#f5f1e6"}]},{"featureType":"administrative","elementType":"geometry.stroke","stylers":[{"color":"#c9b2a6"}]},{"featureType":"administrative.land_parcel","elementType":"geometry.stroke","stylers":[{"color":"#dcd2be"}]},{"featureType":"administrative.land_parcel","elementType":"labels.text.fill","stylers":[{"color":"#ae9e90"}]},{"featureType":"landscape.natural","elementType":"geometry","stylers":[{"color":"#dfd2ae"}]},{"featureType":"poi","elementType":"geometry","stylers":[{"color":"#dfd2ae"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#93817c"}]},{"featureType":"poi.park","elementType":"geometry.fill","stylers":[{"color":"#a5b076"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#447530"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#f5f1e6"}]},{"featureType":"road.arterial","elementType":"geometry","stylers":[{"color":"#fdfcf8"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#f8c967"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#e9bc62"}]},{"featureType":"road.highway.controlled_access","elementType":"geometry","stylers":[{"color":"#e98d58"}]},{"featureType":"road.highway.controlled_access","elementType":"geometry.stroke","stylers":[{"color":"#db8555"}]},{"featureType":"road.local","elementType":"labels.text.fill","stylers":[{"color":"#806b63"}]},{"featureType":"transit.line","elementType":"geometry","stylers":[{"color":"#dfd2ae"}]},{"featureType":"transit.line","elementType":"labels.text.fill","stylers":[{"color":"#8f7d77"}]},{"featureType":"transit.line","elementType":"labels.text.stroke","stylers":[{"color":"#ebe3cd"}]},{"featureType":"transit.station","elementType":"geometry","stylers":[{"color":"#dfd2ae"}]},{"featureType":"water","elementType":"geometry.fill","stylers":[{"color":"#b9d3c2"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#92998d"}]}]',
// GoogleMapStyle.dark:
// r'[{"elementType":"geometry","stylers":[{"color":"#212121"}]},{"elementType":"labels.icon","stylers":[{"visibility":"off"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#212121"}]},{"featureType":"administrative","elementType":"geometry","stylers":[{"color":"#757575"}]},{"featureType":"administrative.country","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]},{"featureType":"administrative.land_parcel","stylers":[{"visibility":"off"}]},{"featureType":"administrative.locality","elementType":"labels.text.fill","stylers":[{"color":"#bdbdbd"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#181818"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"featureType":"poi.park","elementType":"labels.text.stroke","stylers":[{"color":"#1b1b1b"}]},{"featureType":"road","elementType":"geometry.fill","stylers":[{"color":"#2c2c2c"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#8a8a8a"}]},{"featureType":"road.arterial","elementType":"geometry","stylers":[{"color":"#373737"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#3c3c3c"}]},{"featureType":"road.highway.controlled_access","elementType":"geometry","stylers":[{"color":"#4e4e4e"}]},{"featureType":"road.local","elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"featureType":"transit","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#000000"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#3d3d3d"}]}]',
// GoogleMapStyle.night:
// r'[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#746855"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#242f3e"}]},{"featureType":"administrative.locality","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#263c3f"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#6b9a76"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#38414e"}]},{"featureType":"road","elementType":"geometry.stroke","stylers":[{"color":"#212a37"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#9ca5b3"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#746855"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#1f2835"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#f3d19c"}]},{"featureType":"transit","elementType":"geometry","stylers":[{"color":"#2f3948"}]},{"featureType":"transit.station","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#17263c"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#515c6d"}]},{"featureType":"water","elementType":"labels.text.stroke","stylers":[{"color":"#17263c"}]}]',
// GoogleMapStyle.aubergine:
// r'[{"elementType":"geometry","stylers":[{"color":"#1d2c4d"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#8ec3b9"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#1a3646"}]},{"featureType":"administrative.country","elementType":"geometry.stroke","stylers":[{"color":"#4b6878"}]},{"featureType":"administrative.land_parcel","elementType":"labels.text.fill","stylers":[{"color":"#64779e"}]},{"featureType":"administrative.province","elementType":"geometry.stroke","stylers":[{"color":"#4b6878"}]},{"featureType":"landscape.man_made","elementType":"geometry.stroke","stylers":[{"color":"#334e87"}]},{"featureType":"landscape.natural","elementType":"geometry","stylers":[{"color":"#023e58"}]},{"featureType":"poi","elementType":"geometry","stylers":[{"color":"#283d6a"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#6f9ba5"}]},{"featureType":"poi","elementType":"labels.text.stroke","stylers":[{"color":"#1d2c4d"}]},{"featureType":"poi.park","elementType":"geometry.fill","stylers":[{"color":"#023e58"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#3C7680"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#304a7d"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#98a5be"}]},{"featureType":"road","elementType":"labels.text.stroke","stylers":[{"color":"#1d2c4d"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#2c6675"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#255763"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#b0d5ce"}]},{"featureType":"road.highway","elementType":"labels.text.stroke","stylers":[{"color":"#023e58"}]},{"featureType":"transit","elementType":"labels.text.fill","stylers":[{"color":"#98a5be"}]},{"featureType":"transit","elementType":"labels.text.stroke","stylers":[{"color":"#1d2c4d"}]},{"featureType":"transit.line","elementType":"geometry.fill","stylers":[{"color":"#283d6a"}]},{"featureType":"transit.station","elementType":"geometry","stylers":[{"color":"#3a4762"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#0e1626"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#4e6d70"}]}]',
// };
// Map<GoogleMarkerColor, double> googleMarkerColorMap = {
// GoogleMarkerColor.red: 0.0,
// GoogleMarkerColor.orange: 30.0,
// GoogleMarkerColor.yellow: 60.0,
// GoogleMarkerColor.green: 120.0,
// GoogleMarkerColor.cyan: 180.0,
// GoogleMarkerColor.azure: 210.0,
// GoogleMarkerColor.blue: 240.0,
// GoogleMarkerColor.violet: 270.0,
// GoogleMarkerColor.magenta: 300.0,
// GoogleMarkerColor.rose: 330.0,
// };

View File

@@ -0,0 +1,185 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
/// A customizable icon button widget.
class FlutterFlowIconButton extends StatefulWidget {
/// Creates a [FlutterFlowIconButton].
///
/// - [icon] parameter is required and specifies the widget to be used as the icon.
/// - [borderRadius] parameter specifies the border radius of the button.
/// - [buttonSize] parameter specifies the size of the button.
/// - [fillColor] parameter specifies the fill color of the button.
/// - [disabledColor] parameter specifies the color of the button when it is disabled.
/// - [disabledIconColor] parameter specifies the color of the icon when the button is disabled.
/// - [hoverColor] parameter specifies the color of the button when it is hovered.
/// - [hoverIconColor] parameter specifies the color of the icon when the button is hovered.
/// - [borderColor] parameter specifies the border color of the button.
/// - [borderWidth] parameter specifies the width of the button's border.
/// - [showLoadingIndicator] parameter specifies whether to show a loading indicator on the button.
/// - [onPressed] parameter specifies the callback function to be called when the button is pressed.
const FlutterFlowIconButton({
super.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,
});
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<FlutterFlowIconButton> createState() => _FlutterFlowIconButtonState();
}
class _FlutterFlowIconButtonState extends State<FlutterFlowIconButton> {
bool loading = false;
late double? iconSize;
late Color? iconColor;
late Widget effectiveIcon;
@override
void initState() {
super.initState();
_updateIcon();
}
@override
void didUpdateWidget(FlutterFlowIconButton 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: WidgetStateProperty.resolveWith<OutlinedBorder>(
(states) {
return RoundedRectangleBorder(
borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
side: BorderSide(
color: widget.borderColor ?? Colors.transparent,
width: widget.borderWidth ?? 0,
),
);
},
),
iconColor: WidgetStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(WidgetState.disabled) &&
widget.disabledIconColor != null) {
return widget.disabledIconColor;
}
if (states.contains(WidgetState.hovered) &&
widget.hoverIconColor != null) {
return widget.hoverIconColor;
}
return iconColor;
},
),
backgroundColor: WidgetStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(WidgetState.disabled) &&
widget.disabledColor != null) {
return widget.disabledColor;
}
if (states.contains(WidgetState.hovered) &&
widget.hoverColor != null) {
return widget.hoverColor;
}
return widget.fillColor;
},
),
overlayColor: WidgetStateProperty.resolveWith<Color?>((states) {
if (states.contains(WidgetState.pressed)) {
return null;
}
return widget.hoverColor == null ? null : Colors.transparent;
}),
);
return SizedBox(
width: widget.buttonSize,
height: widget.buttonSize,
child: Theme(
data: ThemeData.from(
colorScheme: Theme.of(context).colorScheme,
useMaterial3: true,
),
child: IgnorePointer(
ignoring: widget.showLoadingIndicator && loading,
child: IconButton(
icon: (widget.showLoadingIndicator && loading)
? SizedBox(
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,606 @@
/*
* Copyright (c) 2019 gomgom. https://www.gomgom.net
*
* Source code has been modified by FlutterFlow, Inc. and the below license
* applies only to this file. Adapted from "language_picker" pub.dev package.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import 'package:emoji_flag_converter/emoji_flag_converter.dart';
import 'package:flutter/material.dart';
class FlutterFlowLanguageSelector extends StatelessWidget {
const FlutterFlowLanguageSelector({
super.key,
required this.currentLanguage,
required this.languages,
required this.onChanged,
this.width,
this.height,
this.backgroundColor,
this.borderColor = const Color(0xFF262D34),
this.borderRadius = 8.0,
this.textStyle,
this.hideFlags = false,
this.flagSize = 24.0,
this.flagTextGap = 8.0,
this.dropdownColor,
this.dropdownIconColor = const Color(0xFF14181B),
this.dropdownIcon,
});
final double? width;
final double? height;
final String currentLanguage;
final List<String> languages;
final Function(String) onChanged;
final Color? backgroundColor;
final Color? borderColor;
final double borderRadius;
final TextStyle? textStyle;
final bool hideFlags;
final double flagSize;
final double? flagTextGap;
final Color? dropdownColor;
final Color? dropdownIconColor;
final IconData? dropdownIcon;
@override
Widget build(BuildContext context) => SizedBox(
width: width,
height: height,
child: _LanguagePickerDropdown(
currentLanguage: currentLanguage,
languages: _languageMap(languages.toSet()),
onChanged: onChanged,
backgroundColor: backgroundColor,
borderColor: borderColor,
borderRadius: borderRadius,
dropdownColor: dropdownColor,
dropdownIconColor: dropdownIconColor,
dropdownIcon: dropdownIcon,
itemBuilder: (language) => _LanguagePickerItem(
language: language.isoCode,
languages: languages,
textStyle: textStyle,
hideFlags: hideFlags,
flagSize: flagSize,
flagTextGap: flagTextGap,
),
),
);
}
class _LanguagePickerItem extends StatelessWidget {
const _LanguagePickerItem({
required this.language,
required this.languages,
this.textStyle,
this.hideFlags = false,
this.flagSize = 24.0,
this.flagTextGap = 8.0,
});
final String language;
final List<String> languages;
final TextStyle? textStyle;
final bool hideFlags;
final double flagSize;
final double? flagTextGap;
@override
Widget build(BuildContext context) {
final flagInfo = languageToCountryInfo[language];
Widget flagWidget = Container();
if (flagInfo is String) {
final flagEmoji = EmojiConverter.fromAlpha2CountryCode(flagInfo);
flagWidget = Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Text(
flagEmoji,
style: const TextStyle(fontSize: 20.0),
),
);
} else if (flagInfo is Map) {
final flagUrl = flagInfo['flag'] as String;
flagWidget = Image.network(
flagUrl,
width: 24,
height: 20,
);
}
flagWidget = Transform.scale(
scale: flagSize / 24.0,
child: SizedBox(
width: 24,
child: flagWidget,
),
);
return Row(
children: [
if (!hideFlags) ...[
flagWidget,
SizedBox(width: flagTextGap),
],
Text(
_languageMap(languages.toSet())[language]?.name ?? '',
style: textStyle ??
const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.normal,
),
),
],
);
}
}
/// Provides a customizable [DropdownButton] for all languages
class _LanguagePickerDropdown extends StatelessWidget {
const _LanguagePickerDropdown({
required this.itemBuilder,
required this.currentLanguage,
required this.onChanged,
required this.languages,
this.backgroundColor,
this.borderColor = const Color(0xFF262D34),
this.borderRadius = 8.0,
this.dropdownColor,
this.dropdownIconColor = const Color(0xFF14181B),
this.dropdownIcon,
});
/// This function will be called to build the child of DropdownMenuItem.
final Widget Function(Language) itemBuilder;
/// The current ISO ALPHA-2 code.
final String currentLanguage;
/// This function will be called whenever a Language item is selected.
final ValueChanged<String> onChanged;
/// List of languages available in this picker.
final Map<String, Language> languages;
final Color? backgroundColor;
final Color? borderColor;
final double borderRadius;
final Color? dropdownColor;
final Color? dropdownIconColor;
final IconData? dropdownIcon;
@override
Widget build(BuildContext context) {
List<DropdownMenuItem<String>> items = languages.values
.map(
(language) => DropdownMenuItem<String>(
value: language.isoCode,
child: itemBuilder(language),
),
)
.toList();
return Container(
height: 44.0,
decoration: BoxDecoration(
color: backgroundColor,
border: Border.all(color: borderColor ?? Colors.transparent),
borderRadius: BorderRadius.circular(borderRadius),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: Center(
child: DropdownButton<String>(
isExpanded: true,
underline: Container(),
dropdownColor: dropdownColor ?? backgroundColor,
focusColor: Colors.transparent,
iconEnabledColor: dropdownIconColor,
iconDisabledColor: dropdownIconColor,
icon: dropdownIcon != null
? Icon(
dropdownIcon,
size: 18.0,
color: dropdownIconColor,
)
: null,
hint: const Text(
'Unset',
style: TextStyle(
color: Colors.red,
fontFamily: 'Product Sans',
fontStyle: FontStyle.italic,
fontSize: 15,
),
),
onChanged: (val) {
if (val != null) {
onChanged(val);
}
},
items: items,
value: currentLanguage.isNotEmpty ? currentLanguage : null,
),
),
),
);
}
}
class Language {
Language(this.isoCode, this.name);
Language.fromMap(Map<String, String> map)
: name = map['name']!,
isoCode = map['isoCode']!;
final String name;
final String isoCode;
}
Map<String, Language> _languageMap(Set<String> languages) => Map.fromEntries(
_defaultLanguagesList
.where((element) => languages.contains(element['isoCode']))
.map((e) => MapEntry(e['isoCode']!, Language.fromMap(e))),
);
final List<Map<String, String>> _defaultLanguagesList = [
{"isoCode": "aa", "name": "Afaraf"},
{"isoCode": "af", "name": "Afrikaans"},
{"isoCode": "ak", "name": "Akan"},
{"isoCode": "sq", "name": "Shqip"},
{"isoCode": "am", "name": "አማርኛ"},
{"isoCode": "ar", "name": "العربية"},
{"isoCode": "hy", "name": "Հայերեն"},
{"isoCode": "as", "name": "অসমীয়া"},
{"isoCode": "ay", "name": "aymar"},
{"isoCode": "az", "name": "azərbaycan"},
{"isoCode": "bm", "name": "bamanankan"},
{"isoCode": "ba", "name": "башҡорт теле"},
{"isoCode": "eu", "name": "euskara, euskera"},
{"isoCode": "be", "name": "беларуская мова"},
{"isoCode": "bn", "name": "বাংলা"},
{"isoCode": "bh", "name": "भोजपुरी"},
{"isoCode": "bi", "name": "Bislama"},
{"isoCode": "nb", "name": "Norsk bokmål"},
{"isoCode": "bs", "name": "bosanski jezik"},
{"isoCode": "br", "name": "brezhoneg"},
{"isoCode": "bg", "name": "български език"},
{"isoCode": "my", "name": "ဗမာစာ"},
{"isoCode": "ca", "name": "català"},
{"isoCode": "km", "name": "ភាសាខ្មែរ"},
{"isoCode": "ch", "name": "Chamoru"},
{"isoCode": "ce", "name": "нохчийн мотт"},
{"isoCode": "ny", "name": "chiCheŵa"},
{"isoCode": "zh_Hans", "name": "中文 (简体)"},
{"isoCode": "zh_Hant", "name": "中文 (繁體)"},
{"isoCode": "cv", "name": "чӑваш чӗлхи"},
{"isoCode": "cr", "name": "ᓀᐦᐃᔭᐍᐏᐣ"},
{"isoCode": "hr", "name": "hrvatski jezik"},
{"isoCode": "cs", "name": "čeština"},
{"isoCode": "da", "name": "dansk"},
{"isoCode": "dv", "name": "ދިވެހި"},
{"isoCode": "nl", "name": "Nederlands"},
{"isoCode": "dz", "name": "རྫོང་ཁ"},
{"isoCode": "en", "name": "English"},
{"isoCode": "eo", "name": "Esperanto"},
{"isoCode": "et", "name": "eesti"},
{"isoCode": "ee", "name": "Eʋegbe"},
{"isoCode": "fo", "name": "føroyskt"},
{"isoCode": "fj", "name": "vosa Vakaviti"},
{"isoCode": "fi", "name": "suomi"},
{"isoCode": "fr", "name": "français"},
{"isoCode": "ff", "name": "Fulfulde"},
{"isoCode": "gd", "name": "Gàidhlig"},
{"isoCode": "gl", "name": "galego"},
{"isoCode": "lg", "name": "Luganda"},
{"isoCode": "ka", "name": "ქართული"},
{"isoCode": "de", "name": "Deutsch"},
{"isoCode": "el", "name": "ελληνικά"},
{"isoCode": "gn", "name": "Avañe'ẽ"},
{"isoCode": "gu", "name": "ગુજરાતી"},
{"isoCode": "ht", "name": "Kreyòl ayisyen"},
{"isoCode": "ha", "name": "هَوُسَ"},
{"isoCode": "he", "name": "עברית"},
{"isoCode": "hz", "name": "Otjiherero"},
{"isoCode": "hi", "name": "हिन्दी, हिंदी"},
{"isoCode": "ho", "name": "Hiri Motu"},
{"isoCode": "hu", "name": "magyar"},
{"isoCode": "is", "name": "Íslenska"},
{"isoCode": "io", "name": "Ido"},
{"isoCode": "ig", "name": "Asụsụ Igbo"},
{"isoCode": "id", "name": "Bahasa Indonesia"},
{"isoCode": "ia", "name": "Interlingua"},
{"isoCode": "ie", "name": "Interlingue"},
{"isoCode": "iu", "name": "ᐃᓄᒃᑎᑐᑦ"},
{"isoCode": "ik", "name": "Iñupiaq"},
{"isoCode": "ga", "name": "Gaeilge"},
{"isoCode": "it", "name": "Italiano"},
{"isoCode": "ja", "name": "日本語 (にほんご)"},
{"isoCode": "jv", "name": "ꦧꦱꦗꦮ"},
{"isoCode": "kl", "name": "kalaallisut"},
{"isoCode": "kn", "name": "ಕನ್ನಡ"},
{"isoCode": "kr", "name": "Kanuri"},
{"isoCode": "ks", "name": "कश्मीरी"},
{"isoCode": "kk", "name": "қазақ тілі"},
{"isoCode": "ki", "name": "Gĩkũyũ"},
{"isoCode": "rw", "name": "Ikinyarwanda"},
{"isoCode": "ky", "name": "Кыргызча"},
{"isoCode": "kv", "name": "коми кыв"},
{"isoCode": "kg", "name": "Kikongo"},
{"isoCode": "ko", "name": "한국어"},
{"isoCode": "kj", "name": "Kuanyama"},
{"isoCode": "ku", "name": "Kurdî"},
{"isoCode": "lo", "name": "ພາສາລາວ"},
{"isoCode": "la", "name": "latine"},
{"isoCode": "lv", "name": "latviešu valoda"},
{"isoCode": "li", "name": "Limburgs"},
{"isoCode": "ln", "name": "Lingála"},
{"isoCode": "lt", "name": "lietuvių kalba"},
{"isoCode": "lu", "name": "Tshiluba"},
{"isoCode": "lb", "name": "Lëtzebuergesch"},
{"isoCode": "mk", "name": "македонски јазик"},
{"isoCode": "mg", "name": "fiteny malagasy"},
{"isoCode": "ms", "name": "bahasa Melayu"},
{"isoCode": "ml", "name": "മലയാളം"},
{"isoCode": "mt", "name": "Malti"},
{"isoCode": "gv", "name": "Gaelg, Gailck"},
{"isoCode": "mi", "name": "te reo Māori"},
{"isoCode": "mr", "name": "मराठी"},
{"isoCode": "mh", "name": "Kajin M̧ajeļ"},
{"isoCode": "mn", "name": "Монгол хэл"},
{"isoCode": "na", "name": "Dorerin Naoero"},
{"isoCode": "nv", "name": "Diné bizaad"},
{"isoCode": "nd", "name": "Ndebele (Southern)"},
{"isoCode": "nr", "name": "Ndebele (Northern)"},
{"isoCode": "ng", "name": "Owambo"},
{"isoCode": "ne", "name": "नेपाली"},
{"isoCode": "se", "name": "Davvisámegiella"},
{"isoCode": "no", "name": "Norsk"},
{"isoCode": "nn", "name": "Norsk nynorsk"},
{"isoCode": "oc", "name": "occitan"},
{"isoCode": "oj", "name": "ᐊᓂᔑᓈᐯᒧᐎᓐ"},
{"isoCode": "or", "name": "ଓଡ଼ିଆ"},
{"isoCode": "om", "name": "Afaan Oromoo"},
{"isoCode": "os", "name": "ирон æвзаг"},
{"isoCode": "pi", "name": "पाऴि"},
{"isoCode": "pa", "name": "ਪੰਜਾਬੀ"},
{"isoCode": "fa", "name": "فارسی"},
{"isoCode": "pl", "name": "Polski"},
{"isoCode": "pt", "name": "Português"},
{"isoCode": "ps", "name": "پښتو"},
{"isoCode": "qu", "name": "Runa Simi, Kichwa"},
{"isoCode": "ro", "name": "Română"},
{"isoCode": "rm", "name": "rumantsch grischun"},
{"isoCode": "rn", "name": "Ikirundi"},
{"isoCode": "ru", "name": "Русский"},
{"isoCode": "sm", "name": "gagana fa'a Samoa"},
{"isoCode": "sg", "name": "yângâ tî sängö"},
{"isoCode": "sa", "name": "संस्कृतम्"},
{"isoCode": "sc", "name": "sardu"},
{"isoCode": "sr", "name": "српски језик"},
{"isoCode": "sn", "name": "chiShona"},
{"isoCode": "ii", "name": "ꆈꌠ꒿ Nuosuhxop"},
{"isoCode": "sd", "name": "सिन्धी"},
{"isoCode": "si", "name": "සිංහල"},
{"isoCode": "sk", "name": "slovenský jazyk"},
{"isoCode": "sl", "name": "slovenščina"},
{"isoCode": "so", "name": "Soomaaliga"},
{"isoCode": "st", "name": "Sesotho"},
{"isoCode": "es", "name": "Español"},
{"isoCode": "su", "name": "Basa Sunda"},
{"isoCode": "sw", "name": "Kiswahili"},
{"isoCode": "ss", "name": "SiSwati"},
{"isoCode": "sv", "name": "svenska"},
{"isoCode": "tl", "name": "Tagalog"},
{"isoCode": "ty", "name": "Reo Tahiti"},
{"isoCode": "tg", "name": "тоҷикӣ"},
{"isoCode": "ta", "name": "தமிழ்"},
{"isoCode": "tt", "name": "татар теле"},
{"isoCode": "te", "name": "తెలుగు"},
{"isoCode": "th", "name": "ไทย"},
{"isoCode": "bo", "name": "བོད་ཡིག"},
{"isoCode": "ti", "name": "ትግርኛ"},
{"isoCode": "to", "name": "faka Tonga"},
{"isoCode": "ts", "name": "Xitsonga"},
{"isoCode": "tn", "name": "Setswana"},
{"isoCode": "tr", "name": "Türkçe"},
{"isoCode": "tk", "name": "Түркмен"},
{"isoCode": "tw", "name": "Twi"},
{"isoCode": "ug", "name": "ئۇيغۇرچە"},
{"isoCode": "uk", "name": "Українська"},
{"isoCode": "ur", "name": "اردو"},
{"isoCode": "uz", "name": "Oʻzbek"},
{"isoCode": "ve", "name": "Tshivenḓa"},
{"isoCode": "vi", "name": "Tiếng Việt"},
{"isoCode": "vo", "name": "Volapük"},
{"isoCode": "wa", "name": "walon"},
{"isoCode": "cy", "name": "Cymraeg"},
{"isoCode": "fy", "name": "Frysk"},
{"isoCode": "wo", "name": "Wollof"},
{"isoCode": "xh", "name": "Xhosa"},
{"isoCode": "yi", "name": "ייִדיש"},
{"isoCode": "yo", "name": "Yorùbá"},
{"isoCode": "za", "name": "Saɯ cueŋƅ"},
{"isoCode": "zu", "name": "Zulu"},
];
final Map<String, dynamic> languageToCountryInfo = {
"aa": "dj",
"af": "za",
"ak": "gh",
"sq": "al",
"am": "et",
"ar": {
"proposed_iso_3166": "aa",
"flag":
"https://upload.wikimedia.org/wikipedia/commons/thumb/2/2b/Flag_of_the_Arab_League.svg/400px-Flag_of_the_Arab_League.svg.png",
"name": "Arab League"
},
"hy": "am",
"ay": {
"proposed_iso_3166": "wh",
"flag":
"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Banner_of_the_Qulla_Suyu.svg/1920px-Banner_of_the_Qulla_Suyu.svg.png",
"name": "Wiphala"
},
"az": "az",
"bm": "ml",
"be": "by",
"bn": "bd",
"bi": "vu",
"bs": "ba",
"bg": "bg",
"my": "mm",
"ca": "ad",
"zh": "cn",
"hr": "hr",
"cs": "cz",
"da": "dk",
"dv": "mv",
"nl": "nl",
"dz": "bt",
"en": "gb",
"et": "ee",
"ee": {
"proposed_iso_3166": "ew",
"flag":
"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/Flag_of_the_Ewe_people.svg/2880px-Flag_of_the_Ewe_people.svg.png",
"name": "Ewe"
},
"fj": "fj",
"fil": "ph",
"fi": "fi",
"fr": "fr",
"gaa": "gh",
"ka": "ge",
"kl": "gl",
"de": "de",
"el": "gr",
"gu": "in",
"ht": "ht",
"he": "il",
"hi": "in",
"ho": "pg",
"hu": "hu",
"is": "is",
"ig": "ng",
"id": "id",
"ga": "ie",
"it": "it",
"ja": "jp",
"kr": "ne",
"kk": "kz",
"km": "kh",
"kmb": "ao",
"rw": "rw",
"kg": "cg",
"ko": "kr",
"kj": "ao",
"ku": "iq",
"ky": "kg",
"lo": "la",
"la": "va",
"lv": "lv",
"ln": "cg",
"lt": "lt",
"lu": "cd",
"lb": "lu",
"mk": "mk",
"mg": "mg",
"ms": "my",
"mt": "mt",
"mi": "nz",
"mh": "mh",
"mn": "mn",
"mos": "bf",
"ne": "np",
"nd": "zw",
"nso": "za",
"no": "no",
"nb": "no",
"nn": "no",
"ny": "mw",
"pap": "aw",
"ps": "af",
"fa": "ir",
"pl": "pl",
"pt": "pt",
"pa": "in",
"qu": "wh",
"ro": "ro",
"rm": "ch",
"rn": "bi",
"ru": "ru",
"sg": "cf",
"sr": "rs",
"srr": "sn",
"sn": "zw",
"si": "lk",
"sk": "sk",
"sl": "si",
"so": "so",
"snk": "sn",
"nr": "za",
"st": "ls",
"es": "es",
"sw": {
"proposed_iso_3166": "sw",
"flag":
"https://upload.wikimedia.org/wikipedia/commons/d/de/Flag_of_Swahili.gif",
"name": "Swahili"
},
"ss": "sz",
"sv": "se",
"tl": "ph",
"tg": "tj",
"ta": "lk",
"te": "in",
"tet": "tl",
"th": "th",
"ti": "er",
"tpi": "pg",
"ts": "za",
"tn": "bw",
"tr": "tr",
"tk": "tm",
"uk": "ua",
"umb": "ao",
"ur": "pk",
"uz": "uz",
"ve": "za",
"vi": "vn",
"cy": "gb",
"wo": "sn",
"xh": "za",
"yo": {
"proposed_iso_3166": "yo",
"flag":
"https://upload.wikimedia.org/wikipedia/commons/0/04/Flag_of_the_Yoruba_people.svg",
"name": "Yoruba"
},
"zu": "za",
// Custom
"zh_Hans": "cn",
"zh_Hant": "cn",
"fo": "fo",
"bo": "bo",
"to": "to",
};

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:mime_type/mime_type.dart';
const _kSupportedVideoMimes = {'video/mp4', 'video/mpeg'};
bool _isVideoPath(String path) =>
_kSupportedVideoMimes.contains(mime(path.split('?').first));
class FlutterFlowMediaDisplay extends StatelessWidget {
/// Creates a [FlutterFlowMediaDisplay] widget.
///
/// - [path] parameter specifies the path of the media content.
/// - [imageBuilder] parameter is a function that takes a [String] path and returns a widget to display an image.
/// - [videoPlayerBuilder] parameter is a function that takes a [String] path and returns a widget to display a video player.
const FlutterFlowMediaDisplay({
super.key,
required this.path,
required this.imageBuilder,
required this.videoPlayerBuilder,
});
final String path;
final Widget Function(String) imageBuilder;
final Widget Function(String) videoPlayerBuilder;
@override
Widget build(BuildContext context) =>
_isVideoPath(path) ? videoPlayerBuilder(path) : imageBuilder(path);
}

View File

@@ -0,0 +1,261 @@
import 'package:apivideo_live_stream/apivideo_live_stream.dart';
import 'package:flutter/material.dart';
import 'flutter_flow_widgets.dart';
/// A widget that helps to create a live stream using the Mux API.
class FlutterFlowMuxBroadcast extends StatefulWidget {
const FlutterFlowMuxBroadcast({
super.key,
required this.isCameraInitialized,
required this.isStreaming,
required this.durationString,
this.borderRadius = BorderRadius.zero,
required this.controller,
required this.videoConfig,
required this.onCameraRotateButtonTap,
required this.startButtonText,
required this.onStartButtonTap,
required this.onStopButtonTap,
required this.startButtonOptions,
required this.startButtonIcon,
required this.liveText,
required this.liveTextStyle,
required this.liveIcon,
required this.liveTextBackgroundColor,
this.liveContainerBorderRadius = BorderRadius.zero,
required this.durationTextStyle,
required this.durationTextBackgroundColor,
this.durationContainerBorderRadius = BorderRadius.zero,
required this.rotateButtonIcon,
required this.rotateButtonColor,
required this.stopButtonColor,
required this.stopButtonIcon,
});
/// Whether the camera is initialized or not.
final bool isCameraInitialized;
/// Whether the video is currently being streamed or not.
final bool isStreaming;
/// The duration of the video stream.
final String? durationString;
/// The border radius of the widget.
final BorderRadius borderRadius;
/// The controller for the live stream.
final LiveStreamController? controller;
/// The configuration for the video stream.
final VideoConfig videoConfig;
/// Callback function when the camera rotate button is tapped.
final Function onCameraRotateButtonTap;
/// The text for the start button.
final String startButtonText;
/// Callback function when the start button is tapped.
final Function onStartButtonTap;
/// Callback function when the stop button is tapped.
final Function onStopButtonTap;
/// The options for the start button.
final FFButtonOptions startButtonOptions;
/// The icon for the start button.
final Widget startButtonIcon;
/// The text for the live indicator.
final String liveText;
/// The style for the live indicator text.
final TextStyle liveTextStyle;
/// The icon for the live indicator.
final Widget liveIcon;
/// The background color for the live indicator.
final Color liveTextBackgroundColor;
/// The border radius for the live indicator container.
final BorderRadius liveContainerBorderRadius;
/// The style for the duration text.
final TextStyle durationTextStyle;
/// The background color for the duration text.
final Color durationTextBackgroundColor;
/// The border radius for the duration text container.
final BorderRadius durationContainerBorderRadius;
/// The icon for the rotate button.
final Widget rotateButtonIcon;
/// The color for the rotate button.
final Color rotateButtonColor;
/// The color for the stop button.
final Color stopButtonColor;
/// The icon for the stop button.
final Widget stopButtonIcon;
@override
State<FlutterFlowMuxBroadcast> createState() =>
_FlutterFlowMuxBroadcastState();
}
class _FlutterFlowMuxBroadcastState extends State<FlutterFlowMuxBroadcast>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
widget.controller?.stop();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.inactive) {
final isStreaming = widget.controller?.isStreaming ?? false;
if (isStreaming) {
widget.onStopButtonTap();
}
widget.controller?.stop();
} else if (state == AppLifecycleState.resumed) {
widget.controller?.startPreview();
}
}
@override
Widget build(BuildContext context) {
return widget.isCameraInitialized
? ClipRRect(
borderRadius: widget.borderRadius,
child: CameraPreview(
controller: widget.controller!,
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
bottom: 16.0,
),
child: Stack(
children: [
Align(
alignment: Alignment.bottomLeft,
child: widget.isStreaming
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(50),
child: InkWell(
onTap: () => widget.onStopButtonTap(),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.stopButtonColor,
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: widget.stopButtonIcon,
),
),
),
)
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () => widget.onCameraRotateButtonTap(),
child: CircleAvatar(
radius:
(widget.rotateButtonIcon as Icon).size,
backgroundColor: widget.rotateButtonColor,
child: Center(
child: widget.rotateButtonIcon,
),
),
),
FFButtonWidget(
onPressed: () => widget.onStartButtonTap(),
text: widget.startButtonText,
icon: widget.startButtonIcon,
options: widget.startButtonOptions,
)
],
),
),
widget.isStreaming
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
decoration: BoxDecoration(
color: widget.liveTextBackgroundColor,
borderRadius:
widget.liveContainerBorderRadius,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
widget.liveIcon,
const SizedBox(width: 8),
Text(
widget.liveText,
style: widget.liveTextStyle,
),
],
),
),
),
Container(
decoration: BoxDecoration(
color: widget.durationTextBackgroundColor,
borderRadius:
widget.durationContainerBorderRadius,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(
widget.durationString ?? '00:00:00',
style: widget.durationTextStyle,
),
),
),
],
)
: const SizedBox(),
],
),
),
),
)
: const Center(
child: CircularProgressIndicator(),
);
}
}

View File

@@ -0,0 +1,318 @@
/*
* Copyright 2020 https://github.com/TercyoStorck
*
* Source code has been modified by FlutterFlow, Inc.
*
* 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 'package:flutter/material.dart';
import 'package:flutterflow_ui/src/utils/form_field_controller.dart';
/// A custom radio button widget that allows the user to select a single option from a list of options.
class FlutterFlowRadioButton extends StatefulWidget {
/// Creates a [FlutterFlowRadioButton].
const FlutterFlowRadioButton({
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,
});
/// The list of options to choose from.
final List<String> options;
/// A callback function that will be called when the selected option changes.
final Function(String?)? onChanged;
/// A form field controller that manages the state of the selected option.
final FormFieldController<String> controller;
/// The height of each option.
final double optionHeight;
/// The width of each option. If not provided, the width will be determined automatically.
final double? optionWidth;
/// The style of the option text.
final TextStyle textStyle;
/// The style of the selected option text. If not provided, the [textStyle] will be used.
final TextStyle? selectedTextStyle;
/// The padding around the option text.
final EdgeInsetsGeometry textPadding;
/// The position of the radio button relative to the option text.
final RadioButtonPosition buttonPosition;
/// The direction in which the options are laid out.
final Axis direction;
/// The color of the radio button.
final Color radioButtonColor;
/// The color of the radio button when it is not selected. If not provided, the [radioButtonColor] will be used.
final Color? inactiveRadioButtonColor;
/// Whether the radio button can be toggled on and off.
final bool toggleable;
/// The horizontal alignment of the options when the direction is horizontal.
final WrapAlignment horizontalAlignment;
/// The vertical alignment of the options when the direction is vertical.
final WrapCrossAlignment verticalAlignment;
@override
State<FlutterFlowRadioButton> createState() => _FlutterFlowRadioButtonState();
}
class _FlutterFlowRadioButtonState extends State<FlutterFlowRadioButton> {
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(FlutterFlowRadioButton 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,146 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutterflow_ui/src/utils/lat_lng.dart';
import 'package:mapbox_search/mapbox_search.dart' as mapbox;
/// A widget that displays a static map using the Mapbox API.
class FlutterFlowStaticMap extends StatelessWidget {
const FlutterFlowStaticMap({
super.key,
required this.location,
required this.apiKey,
required this.style,
required this.width,
required this.height,
this.fit,
this.borderRadius = BorderRadius.zero,
this.markerColor,
this.markerUrl,
this.cached = false,
this.zoom = 12,
this.tilt = 0,
this.rotation = 0,
});
/// The location to display on the map.
final LatLng location;
/// The API key for accessing the Mapbox API.
final String apiKey;
/// The style of the map.
final mapbox.MapBoxStyle style;
/// The width of the map widget.
final double width;
/// The height of the map widget.
final double height;
/// How the map should be inscribed into the available space.
final BoxFit? fit;
/// The border radius of the map widget.
final BorderRadius borderRadius;
/// The color of the marker on the map.
final Color? markerColor;
/// The URL of the custom marker icon.
final String? markerUrl;
/// Whether to cache the map image.
final bool cached;
/// The zoom level of the map.
final int zoom;
/// The tilt angle of the map camera.
final int tilt;
/// The rotation angle of the map camera.
final int rotation;
@override
Widget build(BuildContext context) {
final imageWidth = width.clamp(1, 1280).toInt();
final imageHeight = height.clamp(1, 1280).toInt();
final imagePath = getStaticMapImageURL(location, apiKey, style, imageWidth,
imageHeight, markerColor, markerUrl, zoom, rotation, tilt);
return ClipRRect(
borderRadius: borderRadius,
child: cached
? CachedNetworkImage(
imageUrl: imagePath,
width: width,
height: height,
fit: fit,
)
: Image.network(
imagePath,
width: width,
height: height,
fit: fit,
),
);
}
}
String getStaticMapImageURL(
LatLng location,
String apiKey,
mapbox.MapBoxStyle mapStyle,
int width,
int height,
Color? markerColor,
String? markerURL,
int zoom,
int rotation,
int tilt,
) {
final finalLocation = (
lat: location.latitude.clamp(-90, 90).toDouble(),
long: location.longitude.clamp(-180, 180).toDouble(),
);
final finalRotation = rotation.clamp(-180, 180).round();
final finalTilt = tilt.clamp(0, 60).round();
final finalZoom = zoom.clamp(0, 22).round();
final image = mapbox.StaticImage(apiKey: apiKey);
if (markerColor == null && (markerURL == null || markerURL.trim().isEmpty)) {
return image
.getStaticUrlWithoutMarker(
center: finalLocation,
style: mapStyle,
width: width.round(),
height: height.round(),
zoomLevel: finalZoom,
bearing: finalRotation,
pitch: finalTilt,
)
.toString();
} else {
return image
.getStaticUrlWithMarker(
marker: markerURL == null || markerURL.trim().isEmpty
? mapbox.MapBoxMarker(
markerColor: mapbox.RgbColor(
markerColor!.red,
markerColor.green,
markerColor.blue,
),
markerLetter: mapbox.MakiIcons.circle.value,
markerSize: mapbox.MarkerSize.MEDIUM,
)
: null,
markerUrl: markerURL,
center: finalLocation,
style: mapStyle,
width: width.round(),
height: height.round(),
zoomLevel: finalZoom,
bearing: finalRotation,
pitch: finalTilt,
)
.toString();
}
}

View File

@@ -0,0 +1,97 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
/// A widget that displays a stack of swipeable cards.
class FlutterFlowSwipeableStack extends StatefulWidget {
/// Creates a [FlutterFlowSwipeableStack].
///
/// - [itemBuilder] is a callback that builds the widget for each card in the stack.
/// - [itemCount] is the total number of cards in the stack.
/// - [controller] is the controller for the swipeable stack.
/// - [onSwipeFn] is a callback that is called when a card is swiped.
/// - [onRightSwipe] is a callback that is called when a card is swiped to the right.
/// - [onLeftSwipe] is a callback that is called when a card is swiped to the left.
/// - [onUpSwipe] is a callback that is called when a card is swiped up.
/// - [onDownSwipe] is a callback that is called when a card is swiped down.
/// - [loop] determines whether the stack should loop back to the beginning when the last card is swiped.
/// - [cardDisplayCount] is the number of cards to display on the stack at a time.
/// - [scale] is the scale factor for the cards in the stack.
/// - [maxAngle] is the maximum rotation angle for the cards in the stack.
/// - [threshold] is the swipe threshold for the cards in the stack.
/// - [cardPadding] is the padding for each card in the stack.
/// - [backCardOffset] is the offset for the back card in the stack.
const FlutterFlowSwipeableStack({
super.key,
required this.itemBuilder,
required this.itemCount,
required this.controller,
required this.onSwipeFn,
required this.onRightSwipe,
required this.onLeftSwipe,
required this.onUpSwipe,
required this.onDownSwipe,
required this.loop,
required this.cardDisplayCount,
required this.scale,
this.maxAngle,
this.threshold,
this.cardPadding,
this.backCardOffset,
});
final Widget Function(BuildContext, int) itemBuilder;
final CardSwiperController controller;
final int itemCount;
final Function(int) onSwipeFn;
final Function(int) onRightSwipe;
final Function(int) onLeftSwipe;
final Function(int) onUpSwipe;
final Function(int) onDownSwipe;
final bool loop;
final int cardDisplayCount;
final double scale;
final double? maxAngle;
final double? threshold;
final EdgeInsetsGeometry? cardPadding;
final Offset? backCardOffset;
@override
State<FlutterFlowSwipeableStack> createState() => _FFSwipeableStackState();
}
class _FFSwipeableStackState extends State<FlutterFlowSwipeableStack> {
@override
Widget build(BuildContext context) {
return CardSwiper(
controller: widget.controller,
onSwipe: (previousIndex, currentIndex, direction) {
widget.onSwipeFn(previousIndex);
if (direction == CardSwiperDirection.left) {
widget.onLeftSwipe(previousIndex);
} else if (direction == CardSwiperDirection.right) {
widget.onRightSwipe(previousIndex);
} else if (direction == CardSwiperDirection.top) {
widget.onUpSwipe(previousIndex);
} else if (direction == CardSwiperDirection.bottom) {
widget.onDownSwipe(previousIndex);
}
return true;
},
cardsCount: widget.itemCount,
cardBuilder: (context, index, percentThresholdX, percentThresholdY) {
return widget.itemBuilder(context, index);
},
isLoop: widget.loop,
maxAngle: widget.maxAngle ?? 30,
threshold:
widget.threshold != null ? (100 * widget.threshold!).round() : 50,
scale: widget.scale,
padding: widget.cardPadding ??
const EdgeInsets.symmetric(horizontal: 20, vertical: 25),
backCardOffset: widget.backCardOffset ?? const Offset(0, 40),
numberOfCardsDisplayed: min(widget.cardDisplayCount, widget.itemCount),
);
}
}

View File

@@ -0,0 +1,144 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:stop_watch_timer/stop_watch_timer.dart';
// Simple wrapper around StopWatchTimer that emits notifications on events.
class FlutterFlowTimerController with ChangeNotifier {
FlutterFlowTimerController(this.timer);
final StopWatchTimer timer;
void onStartTimer() {
timer.onStartTimer();
notifyListeners();
}
void onStopTimer() {
timer.onStopTimer();
notifyListeners();
}
void onResetTimer() {
timer.onResetTimer();
late final StreamSubscription subscription;
// We can't notify listeners right away: they'll see the old timer value.
// We need to wait until the next time is emitted.
subscription = timer.rawTime.listen((_) {
notifyListeners();
subscription.cancel();
});
}
@override
void dispose() {
timer.dispose();
super.dispose();
}
}
/// A timer widget that displays and manages time.
class FlutterFlowTimer extends StatefulWidget {
/// Creates a [FlutterFlowTimer] widget.
const FlutterFlowTimer({
super.key,
required this.initialTime,
required this.controller,
required this.getDisplayTime,
required this.onChanged,
this.updateStateInterval,
this.onEnded,
required this.textAlign,
required this.style,
});
/// The initial time for the timer.
final int initialTime;
/// The controller for the timer.
final FlutterFlowTimerController controller;
/// A function that returns the formatted display time.
final String Function(int) getDisplayTime;
/// A callback function that is called when the timer value changes.
final Function(int value, String displayTime, bool shouldUpdate) onChanged;
/// The interval at which the timer state should be updated.
final Duration? updateStateInterval;
/// A callback function that is called when the timer ends.
final Function()? onEnded;
/// The alignment of the timer text.
final TextAlign textAlign;
/// The style of the timer text.
final TextStyle style;
@override
State<FlutterFlowTimer> createState() => _FlutterFlowTimerState();
}
class _FlutterFlowTimerState extends State<FlutterFlowTimer> {
int get timerValue => widget.controller.timer.rawTime.value;
bool get isCountUp => widget.controller.timer.mode == StopWatchMode.countUp;
late String _displayTime;
late int lastUpdateMs;
Function() get onEnded => widget.onEnded ?? () {};
void _initTimer({required bool shouldUpdate}) {
// Initialize timer display time and last update time.
_displayTime = widget.getDisplayTime(widget.controller.timer.rawTime.value);
lastUpdateMs = timerValue;
// Update timer value and display time.
widget.onChanged(timerValue, _displayTime, shouldUpdate);
}
@override
void initState() {
super.initState();
// Set the initial time.
widget.controller.timer.setPresetTime(mSec: widget.initialTime, add: false);
// Initialize timer properties without updating outer state.
_initTimer(shouldUpdate: false);
// Add a listener for when the timer value changes to update the
// displayed timer value.
widget.controller.timer.rawTime.listen((_) {
_displayTime = widget.getDisplayTime(timerValue);
widget.onChanged(timerValue, _displayTime, _shouldUpdate());
if (mounted) {
setState(() {});
}
});
// Add listener for actions executed on timer.
widget.controller.addListener(() => _initTimer(shouldUpdate: true));
// Add listener for when the timer ends.
widget.controller.timer.fetchEnded.listen((_) => onEnded());
}
bool _shouldUpdate() {
// If a null or 0ms update interval is provided, always update.
final updateIntervalMs = widget.updateStateInterval?.inMilliseconds;
if (updateIntervalMs == null || updateIntervalMs == 0) {
return true;
}
// Otherwise, we only update after the specified duration has passed
// since the most recent update.
final cutoff = lastUpdateMs + updateIntervalMs * (isCountUp ? 1 : -1);
final shouldUpdate = isCountUp ? timerValue > cutoff : timerValue < cutoff;
if (shouldUpdate) {
lastUpdateMs = timerValue;
}
return shouldUpdate;
}
@override
Widget build(BuildContext context) => Text(
_displayTime,
textAlign: widget.textAlign,
style: widget.style,
);
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
/// A widget that represents a toggle icon.
class ToggleIcon extends StatelessWidget {
/// Creates a [ToggleIcon].
///
/// - [value] parameter specifies whether the icon is currently toggled on or off.
/// - [onPressed] parameter is a callback function that is called when the icon is pressed.
/// - [onIcon] parameter specifies the widget to display when the icon is toggled on.
/// - [offIcon] parameter specifies the widget to display when the icon is toggled off.
const ToggleIcon({
super.key,
required this.value,
required this.onPressed,
required this.onIcon,
required this.offIcon,
});
/// Whether the icon is currently toggled on or off.
final bool value;
/// A callback function that is called when the icon is pressed.
final Function() onPressed;
/// The widget to display when the icon is toggled on.
final Widget onIcon;
/// The widget to display when the icon is toggled off.
final Widget offIcon;
@override
Widget build(BuildContext context) => IconButton(
onPressed: onPressed,
icon: value ? onIcon : offIcon,
);
}

View File

@@ -0,0 +1,140 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutterflow_ui/src/utils/flutter_flow_helpers.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart' hide NavigationDecision;
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webviewx_plus/webviewx_plus.dart';
/// A widget that displays web content in a WebView.
class FlutterFlowWebView extends StatefulWidget {
/// Creates a [FlutterFlowWebView] widget.
///
/// - [content] parameter specifies the web content to be displayed.
/// - [width] and [height] parameters specify the dimensions of the WebView.
/// - [bypass] parameter determines whether to bypass the WebView and open the content in the default browser.
/// - [horizontalScroll] parameter determines whether to enable horizontal scrolling in the WebView.
/// - [verticalScroll] parameter determines whether to enable vertical scrolling in the WebView.
/// - [html] parameter determines whether the content is HTML.
const FlutterFlowWebView({
super.key,
required this.content,
this.width,
this.height,
this.bypass = false,
this.horizontalScroll = false,
this.verticalScroll = false,
this.html = false,
});
/// The web content to be displayed in the WebView.
final String content;
/// The width of the WebView.
final double? width;
/// The height of the WebView.
final double? height;
/// Determines whether to bypass the WebView and open the content in the default browser.
final bool bypass;
/// Determines whether to enable horizontal scrolling in the WebView.
final bool horizontalScroll;
/// Determines whether to enable vertical scrolling in the WebView.
final bool verticalScroll;
/// Determines whether the content is HTML.
final bool html;
@override
State<FlutterFlowWebView> createState() => _FlutterFlowWebViewState();
}
class _FlutterFlowWebViewState extends State<FlutterFlowWebView> {
@override
Widget build(BuildContext context) => WebViewX(
key: webviewKey,
width: widget.width ?? MediaQuery.sizeOf(context).width,
height: widget.height ?? MediaQuery.sizeOf(context).height,
ignoreAllGestures: false,
initialContent: widget.content,
initialMediaPlaybackPolicy:
AutoMediaPlaybackPolicy.requireUserActionForAllMediaTypes,
initialSourceType: widget.html
? SourceType.html
: widget.bypass
? SourceType.urlBypass
: SourceType.url,
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (controller) async {
if (controller.connector is WebViewController && isAndroid) {
final androidController =
controller.connector.platform as AndroidWebViewController;
await androidController.setOnShowFileSelector(_androidFilePicker);
}
},
navigationDelegate: (request) async {
if (isAndroid) {
if (request.content.source
.startsWith('https://api.whatsapp.com/send?phone')) {
String url = request.content.source;
await launchUrl(
Uri.parse(url),
mode: LaunchMode.externalApplication,
);
return NavigationDecision.prevent;
}
}
return NavigationDecision.navigate;
},
webSpecificParams: const WebSpecificParams(
webAllowFullscreenContent: true,
),
mobileSpecificParams: MobileSpecificParams(
debuggingEnabled: false,
gestureNavigationEnabled: true,
mobileGestureRecognizers: {
if (widget.verticalScroll)
const Factory<VerticalDragGestureRecognizer>(
VerticalDragGestureRecognizer.new,
),
if (widget.horizontalScroll)
const Factory<HorizontalDragGestureRecognizer>(
HorizontalDragGestureRecognizer.new,
),
},
androidEnableHybridComposition: true,
),
);
Key get webviewKey => Key(
[
widget.content,
widget.width,
widget.height,
widget.bypass,
widget.horizontalScroll,
widget.verticalScroll,
widget.html,
].map((s) => s?.toString() ?? '').join(),
);
Future<List<String>> _androidFilePicker(
final FileSelectorParams params,
) async {
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
return [file.uri.toString()];
}
return [];
}
}

View File

@@ -0,0 +1,25 @@
// export 'flutter_flow_ad_banner.dart';
export 'flutter_flow_autocomplete_options_list.dart';
export 'flutter_flow_button.dart';
export 'flutter_flow_button_tabbar.dart';
export 'flutter_flow_calendar.dart';
export 'flutter_flow_charts.dart';
export 'flutter_flow_checkbox_group.dart';
export 'flutter_flow_choice_chips.dart';
export 'flutter_flow_count_controller.dart';
export 'flutter_flow_credit_card_form.dart';
export 'flutter_flow_data_table.dart';
export 'flutter_flow_drop_down.dart';
export 'flutter_flow_expanded_image_view.dart';
// export 'flutter_flow_google_map.dart';
export 'flutter_flow_icon_button.dart';
export 'flutter_flow_language_selector.dart';
export 'flutter_flow_media_display.dart';
export 'flutter_flow_mux_broadcast.dart';
export 'flutter_flow_radio_button.dart';
export 'flutter_flow_static_map.dart';
export 'flutter_flow_swipeable_stack.dart';
export 'flutter_flow_timer.dart';
export 'flutter_flow_toggle_icon.dart';
export 'flutter_flow_web_view.dart';
export 'keep_alive_wrapper.dart';

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class KeepAliveWidgetWrapper extends StatefulWidget {
const KeepAliveWidgetWrapper({
super.key,
required this.builder,
});
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);
}
}