基于flutterflow-ui v0.3.1版本 去除google ad 与google map
This commit is contained in:
155
lib/src/constants.dart
Normal file
155
lib/src/constants.dart
Normal 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;
|
||||
103
lib/src/utils/flutter_flow_animations.dart
Normal file
103
lib/src/utils/flutter_flow_animations.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
279
lib/src/utils/flutter_flow_helpers.dart
Normal file
279
lib/src/utils/flutter_flow_helpers.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
lib/src/utils/flutter_flow_model.dart
Normal file
172
lib/src/utils/flutter_flow_model.dart
Normal 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;
|
||||
}
|
||||
71
lib/src/utils/flutter_flow_rive_controller.dart
Normal file
71
lib/src/utils/flutter_flow_rive_controller.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
11
lib/src/utils/flutter_flow_utils.dart
Normal file
11
lib/src/utils/flutter_flow_utils.dart
Normal 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';
|
||||
24
lib/src/utils/form_field_controller.dart
Normal file
24
lib/src/utils/form_field_controller.dart
Normal 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 ?? []);
|
||||
}
|
||||
47
lib/src/utils/internationalization.dart
Normal file
47
lib/src/utils/internationalization.dart
Normal 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));
|
||||
19
lib/src/utils/lat_lng.dart
Normal file
19
lib/src/utils/lat_lng.dart
Normal 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
46
lib/src/utils/place.dart
Normal 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;
|
||||
}
|
||||
51
lib/src/utils/random_data.dart
Normal file
51
lib/src/utils/random_data.dart
Normal 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));
|
||||
}
|
||||
68
lib/src/utils/uploaded_file.dart
Normal file
68
lib/src/utils/uploaded_file.dart
Normal 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;
|
||||
}
|
||||
148
lib/src/widgets/flutter_flow_ad_banner.dart
Normal file
148
lib/src/widgets/flutter_flow_ad_banner.dart
Normal 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;
|
||||
// }
|
||||
// }
|
||||
106
lib/src/widgets/flutter_flow_autocomplete_options_list.dart
Normal file
106
lib/src/widgets/flutter_flow_autocomplete_options_list.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
343
lib/src/widgets/flutter_flow_button.dart
Normal file
343
lib/src/widgets/flutter_flow_button.dart
Normal 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;
|
||||
854
lib/src/widgets/flutter_flow_button_tabbar.dart
Normal file
854
lib/src/widgets/flutter_flow_button_tabbar.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
397
lib/src/widgets/flutter_flow_calendar.dart
Normal file
397
lib/src/widgets/flutter_flow_calendar.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
586
lib/src/widgets/flutter_flow_charts.dart
Normal file
586
lib/src/widgets/flutter_flow_charts.dart
Normal 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);
|
||||
}
|
||||
148
lib/src/widgets/flutter_flow_checkbox_group.dart
Normal file
148
lib/src/widgets/flutter_flow_checkbox_group.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
176
lib/src/widgets/flutter_flow_choice_chips.dart
Normal file
176
lib/src/widgets/flutter_flow_choice_chips.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
lib/src/widgets/flutter_flow_count_controller.dart
Normal file
72
lib/src/widgets/flutter_flow_count_controller.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
305
lib/src/widgets/flutter_flow_credit_card_form.dart
Normal file
305
lib/src/widgets/flutter_flow_credit_card_form.dart
Normal 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.
|
||||
342
lib/src/widgets/flutter_flow_data_table.dart
Normal file
342
lib/src/widgets/flutter_flow_data_table.dart
Normal 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!,
|
||||
);
|
||||
}
|
||||
433
lib/src/widgets/flutter_flow_drop_down.dart
Normal file
433
lib/src/widgets/flutter_flow_drop_down.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
73
lib/src/widgets/flutter_flow_expanded_image_view.dart
Normal file
73
lib/src/widgets/flutter_flow_expanded_image_view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
301
lib/src/widgets/flutter_flow_google_map.dart
Normal file
301
lib/src/widgets/flutter_flow_google_map.dart
Normal 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,
|
||||
// };
|
||||
185
lib/src/widgets/flutter_flow_icon_button.dart
Normal file
185
lib/src/widgets/flutter_flow_icon_button.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
606
lib/src/widgets/flutter_flow_language_selector.dart
Normal file
606
lib/src/widgets/flutter_flow_language_selector.dart
Normal 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",
|
||||
};
|
||||
29
lib/src/widgets/flutter_flow_media_display.dart
Normal file
29
lib/src/widgets/flutter_flow_media_display.dart
Normal 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);
|
||||
}
|
||||
261
lib/src/widgets/flutter_flow_mux_broadcast.dart
Normal file
261
lib/src/widgets/flutter_flow_mux_broadcast.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
318
lib/src/widgets/flutter_flow_radio_button.dart
Normal file
318
lib/src/widgets/flutter_flow_radio_button.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
146
lib/src/widgets/flutter_flow_static_map.dart
Normal file
146
lib/src/widgets/flutter_flow_static_map.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
97
lib/src/widgets/flutter_flow_swipeable_stack.dart
Normal file
97
lib/src/widgets/flutter_flow_swipeable_stack.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
144
lib/src/widgets/flutter_flow_timer.dart
Normal file
144
lib/src/widgets/flutter_flow_timer.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
36
lib/src/widgets/flutter_flow_toggle_icon.dart
Normal file
36
lib/src/widgets/flutter_flow_toggle_icon.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
140
lib/src/widgets/flutter_flow_web_view.dart
Normal file
140
lib/src/widgets/flutter_flow_web_view.dart
Normal 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 [];
|
||||
}
|
||||
}
|
||||
25
lib/src/widgets/flutter_flow_widgets.dart
Normal file
25
lib/src/widgets/flutter_flow_widgets.dart
Normal 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';
|
||||
25
lib/src/widgets/keep_alive_wrapper.dart
Normal file
25
lib/src/widgets/keep_alive_wrapper.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user