├── .DS_Store ├── .gitignore ├── assets ├── package1.gif └── right-click.png ├── lib ├── flutterflow_ui.dart └── src │ ├── utils │ ├── flutter_flow_utils.dart │ ├── lat_lng.dart │ ├── form_field_controller.dart │ ├── place.dart │ ├── internationalization.dart │ ├── random_data.dart │ ├── uploaded_file.dart │ ├── flutter_flow_rive_controller.dart │ ├── flutter_flow_animations.dart │ ├── flutter_flow_model.dart │ └── flutter_flow_helpers.dart │ ├── widgets │ ├── keep_alive_wrapper.dart │ ├── flutter_flow_widgets.dart │ ├── flutter_flow_media_display.dart │ ├── flutter_flow_toggle_icon.dart │ ├── flutter_flow_count_controller.dart │ ├── flutter_flow_expanded_image_view.dart │ ├── flutter_flow_autocomplete_options_list.dart │ ├── flutter_flow_swipeable_stack.dart │ ├── flutter_flow_static_map.dart │ ├── flutter_flow_ad_banner.dart │ ├── flutter_flow_timer.dart │ ├── flutter_flow_web_view.dart │ ├── flutter_flow_choice_chips.dart │ ├── flutter_flow_checkbox_group.dart │ ├── flutter_flow_icon_button.dart │ ├── flutter_flow_mux_broadcast.dart │ ├── flutter_flow_radio_button.dart │ ├── flutter_flow_button.dart │ ├── flutter_flow_data_table.dart │ ├── flutter_flow_credit_card_form.dart │ ├── flutter_flow_calendar.dart │ └── flutter_flow_drop_down.dart │ └── constants.dart ├── CHANGELOG.md ├── LICENSE ├── pubspec.yaml ├── analysis_options.yaml ├── README.md └── .packages /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlutterFlow/flutterflow-ui/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .flutter-plugins-dependencies 3 | .flutter-plugins 4 | pubspec.lock -------------------------------------------------------------------------------- /assets/package1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlutterFlow/flutterflow-ui/HEAD/assets/package1.gif -------------------------------------------------------------------------------- /assets/right-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlutterFlow/flutterflow-ui/HEAD/assets/right-click.png -------------------------------------------------------------------------------- /lib/flutterflow_ui.dart: -------------------------------------------------------------------------------- 1 | library flutterflow_ui; 2 | 3 | export 'src/utils/flutter_flow_utils.dart'; 4 | export 'src/widgets/flutter_flow_widgets.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/utils/flutter_flow_utils.dart: -------------------------------------------------------------------------------- 1 | library flutterflow_ui; 2 | 3 | export 'flutter_flow_animations.dart'; 4 | export 'flutter_flow_helpers.dart'; 5 | export 'flutter_flow_rive_controller.dart'; 6 | export 'form_field_controller.dart'; 7 | export 'internationalization.dart'; 8 | export 'lat_lng.dart'; 9 | export 'place.dart'; 10 | export 'random_data.dart'; 11 | export 'uploaded_file.dart'; 12 | -------------------------------------------------------------------------------- /lib/src/utils/lat_lng.dart: -------------------------------------------------------------------------------- 1 | class LatLng { 2 | const LatLng(this.latitude, this.longitude); 3 | final double latitude; 4 | final double longitude; 5 | 6 | @override 7 | String toString() => 'LatLng(lat: $latitude, lng: $longitude)'; 8 | 9 | String serialize() => '$latitude,$longitude'; 10 | 11 | @override 12 | int get hashCode => latitude.hashCode + longitude.hashCode; 13 | 14 | @override 15 | bool operator ==(other) => 16 | other is LatLng && 17 | latitude == other.latitude && 18 | longitude == other.longitude; 19 | } 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.1 2 | 3 | - Remove audio player support from the package 4 | 5 | ## 0.3.0 6 | 7 | - Add support for new UI widgets available in current FlutterFlow release 8 | - Update utility functions to support new FlutterFlow code generation 9 | - Update dependencies 10 | 11 | ## 0.2.0 12 | 13 | **Breaking changes** 14 | - Remove Flutter media plugins such as chewie, video_player, audio_player, google_maps, webview, mapbox, rxdart, shared_preferences 15 | 16 | **Features** 17 | - Add support for new UI widgets available in current FlutterFlow release 18 | - Add generated code for animations support 19 | 20 | ## 0.1.1 21 | 22 | Removed FlutterFlowTheme from package 23 | 24 | ## 0.1.0+4 25 | 26 | Initial beta release -------------------------------------------------------------------------------- /lib/src/widgets/keep_alive_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class KeepAliveWidgetWrapper extends StatefulWidget { 4 | const KeepAliveWidgetWrapper({ 5 | super.key, 6 | required this.builder, 7 | }); 8 | 9 | final WidgetBuilder builder; 10 | 11 | @override 12 | State createState() => _KeepAliveWidgetWrapperState(); 13 | } 14 | 15 | class _KeepAliveWidgetWrapperState extends State 16 | with AutomaticKeepAliveClientMixin { 17 | @override 18 | bool get wantKeepAlive => true; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | super.build(context); 23 | return widget.builder(context); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/utils/form_field_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | class FormFieldController extends ValueNotifier { 4 | FormFieldController(this.initialValue) : super(initialValue); 5 | 6 | final T? initialValue; 7 | 8 | void reset() => value = initialValue; 9 | 10 | void update() => notifyListeners(); 11 | } 12 | 13 | // If the initial value is a list (which it is for multiselect), 14 | // we need to use this controller to avoid a pass by reference issue 15 | // that can result in the initial value being modified. 16 | class FormListFieldController extends FormFieldController> { 17 | FormListFieldController(super.initialValue) 18 | : _initialListValue = List.from(initialValue ?? []); 19 | 20 | final List? _initialListValue; 21 | 22 | @override 23 | void reset() => value = List.from(_initialListValue ?? []); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_widgets.dart: -------------------------------------------------------------------------------- 1 | export 'flutter_flow_ad_banner.dart'; 2 | export 'flutter_flow_autocomplete_options_list.dart'; 3 | export 'flutter_flow_button.dart'; 4 | export 'flutter_flow_button_tabbar.dart'; 5 | export 'flutter_flow_calendar.dart'; 6 | export 'flutter_flow_charts.dart'; 7 | export 'flutter_flow_checkbox_group.dart'; 8 | export 'flutter_flow_choice_chips.dart'; 9 | export 'flutter_flow_count_controller.dart'; 10 | export 'flutter_flow_credit_card_form.dart'; 11 | export 'flutter_flow_data_table.dart'; 12 | export 'flutter_flow_drop_down.dart'; 13 | export 'flutter_flow_expanded_image_view.dart'; 14 | export 'flutter_flow_google_map.dart'; 15 | export 'flutter_flow_icon_button.dart'; 16 | export 'flutter_flow_language_selector.dart'; 17 | export 'flutter_flow_media_display.dart'; 18 | export 'flutter_flow_mux_broadcast.dart'; 19 | export 'flutter_flow_radio_button.dart'; 20 | export 'flutter_flow_static_map.dart'; 21 | export 'flutter_flow_swipeable_stack.dart'; 22 | export 'flutter_flow_timer.dart'; 23 | export 'flutter_flow_toggle_icon.dart'; 24 | export 'flutter_flow_web_view.dart'; 25 | export 'keep_alive_wrapper.dart'; 26 | -------------------------------------------------------------------------------- /lib/src/utils/place.dart: -------------------------------------------------------------------------------- 1 | import 'lat_lng.dart'; 2 | 3 | class FFPlace { 4 | const FFPlace({ 5 | this.latLng = const LatLng(0.0, 0.0), 6 | this.name = '', 7 | this.address = '', 8 | this.city = '', 9 | this.state = '', 10 | this.country = '', 11 | this.zipCode = '', 12 | }); 13 | 14 | final LatLng latLng; 15 | final String name; 16 | final String address; 17 | final String city; 18 | final String state; 19 | final String country; 20 | final String zipCode; 21 | 22 | @override 23 | String toString() => '''FFPlace( 24 | latLng: $latLng, 25 | name: $name, 26 | address: $address, 27 | city: $city, 28 | state: $state, 29 | country: $country, 30 | zipCode: $zipCode, 31 | )'''; 32 | 33 | @override 34 | int get hashCode => latLng.hashCode; 35 | 36 | @override 37 | bool operator ==(other) => 38 | other is FFPlace && 39 | latLng == other.latLng && 40 | name == other.name && 41 | address == other.address && 42 | city == other.city && 43 | state == other.state && 44 | country == other.country && 45 | zipCode == other.zipCode; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_media_display.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:mime_type/mime_type.dart'; 3 | 4 | const _kSupportedVideoMimes = {'video/mp4', 'video/mpeg'}; 5 | 6 | bool _isVideoPath(String path) => 7 | _kSupportedVideoMimes.contains(mime(path.split('?').first)); 8 | 9 | class FlutterFlowMediaDisplay extends StatelessWidget { 10 | /// Creates a [FlutterFlowMediaDisplay] widget. 11 | /// 12 | /// - [path] parameter specifies the path of the media content. 13 | /// - [imageBuilder] parameter is a function that takes a [String] path and returns a widget to display an image. 14 | /// - [videoPlayerBuilder] parameter is a function that takes a [String] path and returns a widget to display a video player. 15 | const FlutterFlowMediaDisplay({ 16 | super.key, 17 | required this.path, 18 | required this.imageBuilder, 19 | required this.videoPlayerBuilder, 20 | }); 21 | 22 | final String path; 23 | final Widget Function(String) imageBuilder; 24 | final Widget Function(String) videoPlayerBuilder; 25 | 26 | @override 27 | Widget build(BuildContext context) => 28 | _isVideoPath(path) ? videoPlayerBuilder(path) : imageBuilder(path); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_toggle_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A widget that represents a toggle icon. 4 | class ToggleIcon extends StatelessWidget { 5 | /// Creates a [ToggleIcon]. 6 | /// 7 | /// - [value] parameter specifies whether the icon is currently toggled on or off. 8 | /// - [onPressed] parameter is a callback function that is called when the icon is pressed. 9 | /// - [onIcon] parameter specifies the widget to display when the icon is toggled on. 10 | /// - [offIcon] parameter specifies the widget to display when the icon is toggled off. 11 | const ToggleIcon({ 12 | super.key, 13 | required this.value, 14 | required this.onPressed, 15 | required this.onIcon, 16 | required this.offIcon, 17 | }); 18 | 19 | /// Whether the icon is currently toggled on or off. 20 | final bool value; 21 | 22 | /// A callback function that is called when the icon is pressed. 23 | final Function() onPressed; 24 | 25 | /// The widget to display when the icon is toggled on. 26 | final Widget onIcon; 27 | 28 | /// The widget to display when the icon is toggled off. 29 | final Widget offIcon; 30 | 31 | @override 32 | Widget build(BuildContext context) => IconButton( 33 | onPressed: onPressed, 34 | icon: value ? onIcon : offIcon, 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/utils/internationalization.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | //Helper class for internationalization 5 | class FFLocalizations { 6 | FFLocalizations(this.locale); 7 | 8 | final Locale locale; 9 | 10 | //Initializing 11 | static FFLocalizations of(BuildContext context) => 12 | FFLocalizations(const Locale('en')); 13 | 14 | static List languages() => ['en']; 15 | 16 | String get languageCode => locale.languageCode; 17 | 18 | int get languageIndex => languages().contains(languageCode) 19 | ? languages().indexOf(languageCode) 20 | : 0; 21 | 22 | String getText(String key) => 23 | (kTranslationsMap[key] ?? {})[locale.languageCode] ?? ''; 24 | 25 | String getVariableText({ 26 | String? enText = '', 27 | }) => 28 | [enText][languageIndex] ?? ''; 29 | } 30 | 31 | class FFLocalizationsDelegate extends LocalizationsDelegate { 32 | const FFLocalizationsDelegate(); 33 | 34 | @override 35 | bool isSupported(Locale locale) => 36 | FFLocalizations.languages().contains(locale.languageCode); 37 | 38 | @override 39 | Future load(Locale locale) => 40 | SynchronousFuture(FFLocalizations(locale)); 41 | 42 | @override 43 | bool shouldReload(FFLocalizationsDelegate old) => false; 44 | } 45 | 46 | final kTranslationsMap = 47 | >>[].reduce((a, b) => a..addAll(b)); 48 | -------------------------------------------------------------------------------- /lib/src/utils/random_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | final _random = Random(); 6 | 7 | int randomInteger(int min, int max) { 8 | return _random.nextInt(max - min + 1) + min; 9 | } 10 | 11 | double randomDouble(double min, double max) { 12 | return _random.nextDouble() * (max - min) + min; 13 | } 14 | 15 | String randomString( 16 | int minLength, 17 | int maxLength, 18 | bool lowercaseAz, 19 | bool uppercaseAz, 20 | bool digits, 21 | ) { 22 | var chars = ''; 23 | if (lowercaseAz) { 24 | chars += 'abcdefghijklmnopqrstuvwxyz'; 25 | } 26 | if (uppercaseAz) { 27 | chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 28 | } 29 | if (digits) { 30 | chars += '0123456789'; 31 | } 32 | return List.generate(randomInteger(minLength, maxLength), 33 | (index) => chars[_random.nextInt(chars.length)]).join(); 34 | } 35 | 36 | // Random date between 1970 and 2025. 37 | DateTime randomDate() { 38 | // Random max must be in range 0 < max <= 2^32. 39 | // So we have to generate the time in seconds and then convert to milliseconds. 40 | return DateTime.fromMillisecondsSinceEpoch( 41 | randomInteger(0, 1735689600) * 1000); 42 | } 43 | 44 | String randomImageUrl(int width, int height) { 45 | return 'https://picsum.photos/seed/${_random.nextInt(1000)}/$width/$height'; 46 | } 47 | 48 | Color randomColor() { 49 | return Color.fromARGB( 50 | 255, _random.nextInt(255), _random.nextInt(255), _random.nextInt(255)); 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, FlutterFlow 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /lib/src/utils/uploaded_file.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data' show Uint8List; 3 | 4 | class FFUploadedFile { 5 | const FFUploadedFile({ 6 | this.name, 7 | this.bytes, 8 | this.height, 9 | this.width, 10 | this.blurHash, 11 | }); 12 | 13 | final String? name; 14 | final Uint8List? bytes; 15 | final double? height; 16 | final double? width; 17 | final String? blurHash; 18 | 19 | @override 20 | String toString() => 21 | 'FFUploadedFile(name: $name, bytes: ${bytes?.length ?? 0}, height: $height, width: $width, blurHash: $blurHash,)'; 22 | 23 | String serialize() => jsonEncode( 24 | { 25 | 'name': name, 26 | 'bytes': bytes, 27 | 'height': height, 28 | 'width': width, 29 | 'blurHash': blurHash, 30 | }, 31 | ); 32 | 33 | static FFUploadedFile deserialize(String val) { 34 | final serializedData = jsonDecode(val) as Map; 35 | final data = { 36 | 'name': serializedData['name'] ?? '', 37 | 'bytes': serializedData['bytes'] ?? Uint8List.fromList([]), 38 | 'height': serializedData['height'], 39 | 'width': serializedData['width'], 40 | 'blurHash': serializedData['blurHash'], 41 | }; 42 | return FFUploadedFile( 43 | name: data['name'] as String, 44 | bytes: Uint8List.fromList(data['bytes'].cast().toList()), 45 | height: data['height'] as double?, 46 | width: data['width'] as double?, 47 | blurHash: data['blurHash'] as String?, 48 | ); 49 | } 50 | 51 | @override 52 | int get hashCode => Object.hash( 53 | name, 54 | bytes, 55 | height, 56 | width, 57 | blurHash, 58 | ); 59 | 60 | @override 61 | bool operator ==(other) => 62 | other is FFUploadedFile && 63 | name == other.name && 64 | bytes == other.bytes && 65 | height == other.height && 66 | width == other.width && 67 | blurHash == other.blurHash; 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/utils/flutter_flow_rive_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:rive/rive.dart'; 3 | 4 | class FlutterFlowRiveController extends SimpleAnimation { 5 | FlutterFlowRiveController( 6 | super.animationName, { 7 | super.mix, 8 | super.autoplay, 9 | this.shouldLoop = false, 10 | }); 11 | 12 | bool shouldLoop; 13 | final _reactivate = ValueNotifier(false); 14 | ValueListenable get changeReactivate => _reactivate; 15 | 16 | bool get reactivate => _reactivate.value; 17 | set reactivate(bool value) { 18 | if (_reactivate.value != value) { 19 | _reactivate.value = value; 20 | } 21 | } 22 | 23 | bool endOfAnimation(LinearAnimationInstance? instance) { 24 | if (instance == null) { 25 | return false; 26 | } 27 | return instance.time == instance.animation.endTime; 28 | } 29 | 30 | @override 31 | bool init(RuntimeArtboard artboard) { 32 | reactivate = false; 33 | changeReactivate.addListener(() { 34 | if (reactivate) { 35 | isActive = true; 36 | } 37 | }); 38 | return super.init(artboard); 39 | } 40 | 41 | @override 42 | void apply(RuntimeArtboard artboard, double elapsedSeconds) { 43 | if (instance == null) { 44 | return; 45 | } 46 | 47 | /// Reset on button press 48 | if (reactivate) { 49 | if (endOfAnimation(instance)) { 50 | instance?.time = 0; 51 | } 52 | reactivate = false; 53 | } 54 | 55 | if (instance == null || endOfAnimation(instance)) { 56 | isActive = false; 57 | } 58 | 59 | /// Stop after one loop if not a continuous animation 60 | if (!shouldLoop && 61 | (instance?.animation.loop == Loop.loop || 62 | instance?.animation.loop == Loop.pingPong) && 63 | instance!.didLoop) { 64 | isActive = false; 65 | } 66 | 67 | instance! 68 | ..animation.apply(instance!.time, coreContext: artboard, mix: mix) 69 | ..advance(elapsedSeconds); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_count_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FlutterFlowCountController extends StatefulWidget { 4 | const FlutterFlowCountController({ 5 | super.key, 6 | required this.decrementIconBuilder, 7 | required this.incrementIconBuilder, 8 | required this.countBuilder, 9 | required this.count, 10 | required this.updateCount, 11 | this.stepSize = 1, 12 | this.minimum, 13 | this.maximum, 14 | this.contentPadding = const EdgeInsets.symmetric(horizontal: 25.0), 15 | }); 16 | 17 | final Widget Function(bool enabled) decrementIconBuilder; 18 | final Widget Function(bool enabled) incrementIconBuilder; 19 | final Widget Function(int count) countBuilder; 20 | final int count; 21 | final Function(int) updateCount; 22 | final int stepSize; 23 | final int? minimum; 24 | final int? maximum; 25 | final EdgeInsetsGeometry contentPadding; 26 | 27 | @override 28 | State createState() => 29 | _FlutterFlowCountControllerState(); 30 | } 31 | 32 | class _FlutterFlowCountControllerState 33 | extends State { 34 | int get count => widget.count; 35 | int? get minimum => widget.minimum; 36 | int? get maximum => widget.maximum; 37 | int get stepSize => widget.stepSize; 38 | 39 | bool get canDecrement => minimum == null || count - stepSize >= minimum!; 40 | bool get canIncrement => maximum == null || count + stepSize <= maximum!; 41 | 42 | void _decrementCounter() { 43 | if (canDecrement) { 44 | setState(() => widget.updateCount(count - stepSize)); 45 | } 46 | } 47 | 48 | void _incrementCounter() { 49 | if (canIncrement) { 50 | setState(() => widget.updateCount(count + stepSize)); 51 | } 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) => Padding( 56 | padding: widget.contentPadding, 57 | child: Row( 58 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 59 | children: [ 60 | InkWell( 61 | onTap: _decrementCounter, 62 | child: widget.decrementIconBuilder(canDecrement), 63 | ), 64 | widget.countBuilder(count), 65 | InkWell( 66 | onTap: _incrementCounter, 67 | child: widget.incrementIconBuilder(canIncrement), 68 | ), 69 | ], 70 | ), 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutterflow_ui 2 | description: Flutter package that makes it easy to use UI widgets and code generated by FlutterFlow in your Flutter projects. 3 | homepage: https://docs.flutterflow.io 4 | repository: https://github.com/FlutterFlow/flutterflow-ui 5 | version: 0.3.1 6 | 7 | environment: 8 | sdk: ">=3.0.0 <4.0.0" 9 | 10 | dependencies: 11 | aligned_dialog: ^0.0.6 12 | aligned_tooltip: ^0.0.1 13 | apivideo_live_stream: 1.0.7 14 | auto_size_text: ^3.0.0 15 | barcode_widget: ^2.0.3 16 | cached_network_image: ^3.3.1 17 | carousel_slider: ^4.2.1 18 | collection: ^1.18.0 19 | data_table_2: ^2.5.10 20 | dropdown_button2: ^2.3.9 21 | easy_debounce: ^2.0.1 22 | emoji_flag_converter: ^1.1.0 23 | equatable: ^2.0.5 24 | expandable: ^5.0.1 25 | file_picker: ^8.0.3 26 | fl_chart: ^0.68.0 27 | flip_card: ^0.7.0 28 | floating_bottom_navigation_bar: ^1.5.2 29 | flutter: 30 | sdk: flutter 31 | flutter_animate: ^4.5.0 32 | flutter_card_swiper: ^6.0.0 33 | flutter_credit_card: ^4.0.1 34 | flutter_plugin_android_lifecycle: ^2.0.20 35 | flutter_staggered_grid_view: ^0.7.0 36 | font_awesome_flutter: ^10.6.0 37 | from_css_color: ^2.0.0 38 | google_fonts: ^6.1.0 39 | google_maps: ^7.1.0 40 | google_maps_flutter: ^2.6.1 41 | google_maps_flutter_android: ^2.8.1 42 | google_maps_flutter_ios: ^2.6.1 43 | google_maps_flutter_platform_interface: ^2.7.1 44 | google_maps_flutter_web: ^0.5.7 45 | google_mobile_ads: ^5.1.0 46 | http: ^1.2.1 47 | intl: ^0.19.0 48 | json_path: ^0.7.2 49 | mapbox_search: ^4.2.2 50 | mime_type: ^1.0.0 51 | native_device_orientation: ^1.2.1 52 | page_transition: ^2.1.0 53 | percent_indicator: ^4.2.2 54 | photo_view: ^0.14.0 55 | plugin_platform_interface: ^2.1.8 56 | pointer_interceptor: ^0.10.1 57 | provider: ^6.1.2 58 | rive: ^0.12.2 59 | shared_preferences: ^2.2.2 60 | shared_preferences_android: ^2.2.1 61 | shared_preferences_foundation: ^2.3.4 62 | shared_preferences_platform_interface: ^2.3.1 63 | shared_preferences_web: ^2.2.1 64 | smooth_page_indicator: ^1.1.0 65 | stop_watch_timer: ^3.0.2 66 | substring_highlight: ^1.0.33 67 | table_calendar: ^3.1.1 68 | timeago: ^3.6.1 69 | url_launcher: ^6.2.5 70 | url_launcher_android: ^6.3.0 71 | url_launcher_ios: ^6.2.5 72 | url_launcher_platform_interface: ^2.3.2 73 | webview_flutter: ^4.7.0 74 | webview_flutter_android: ^3.15.0 75 | webview_flutter_platform_interface: ^2.10.0 76 | webview_flutter_wkwebview: ^3.12.0 77 | webviewx_plus: ^0.5.0 78 | 79 | dev_dependencies: 80 | flutter_lints: ^4.0.0 81 | 82 | flutter: 83 | uses-material-design: true 84 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_expanded_image_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_view/photo_view.dart'; 3 | 4 | /// A widget that displays an expanded image view. 5 | class FlutterFlowExpandedImageView extends StatelessWidget { 6 | /// Creates a [FlutterFlowExpandedImageView]. 7 | /// 8 | /// - [image] parameter is required and represents the image to be displayed. 9 | /// - [allowRotation] parameter determines whether rotation is allowed for the image. 10 | /// - [useHeroAnimation] parameter determines whether to use a hero animation when transitioning to the expanded image view. 11 | /// - [tag] parameter is an optional tag used for the hero animation. 12 | const FlutterFlowExpandedImageView({ 13 | super.key, 14 | required this.image, 15 | this.allowRotation = false, 16 | this.useHeroAnimation = true, 17 | this.tag, 18 | }); 19 | 20 | final Widget image; 21 | final bool allowRotation; 22 | final bool useHeroAnimation; 23 | final Object? tag; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final screenSize = MediaQuery.sizeOf(context); 28 | return Material( 29 | color: Colors.black, 30 | child: SafeArea( 31 | child: Stack( 32 | children: [ 33 | SizedBox( 34 | height: screenSize.height, 35 | width: screenSize.width, 36 | child: PhotoView.customChild( 37 | minScale: 1.0, 38 | maxScale: 3.0, 39 | enableRotation: allowRotation, 40 | heroAttributes: useHeroAnimation 41 | ? PhotoViewHeroAttributes(tag: tag!) 42 | : null, 43 | onScaleEnd: (context, details, value) { 44 | if (value.scale! < 0.3) { 45 | Navigator.pop(context); 46 | } 47 | }, 48 | child: image, 49 | ), 50 | ), 51 | Row( 52 | mainAxisAlignment: MainAxisAlignment.start, 53 | children: [ 54 | Padding( 55 | padding: const EdgeInsets.only(top: 8.0), 56 | child: IconButton( 57 | color: Colors.black, 58 | onPressed: () => Navigator.pop(context), 59 | icon: const Icon( 60 | Icons.close, 61 | size: 32, 62 | color: Colors.white, 63 | ), 64 | ), 65 | ) 66 | ], 67 | ), 68 | ], 69 | ), 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/utils/flutter_flow_animations.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_animate/flutter_animate.dart'; 3 | 4 | enum AnimationTrigger { 5 | onPageLoad, 6 | onActionTrigger, 7 | } 8 | 9 | class AnimationInfo { 10 | AnimationInfo({ 11 | required this.trigger, 12 | required this.effectsBuilder, 13 | this.loop = false, 14 | this.reverse = false, 15 | this.applyInitialState = true, 16 | }); 17 | 18 | final AnimationTrigger trigger; 19 | final List Function()? effectsBuilder; 20 | final bool applyInitialState; 21 | final bool loop; 22 | final bool reverse; 23 | late AnimationController controller; 24 | 25 | List? _effects; 26 | 27 | List get effects => _effects ??= effectsBuilder!(); 28 | 29 | void maybeUpdateEffects(List? updatedEffects) { 30 | if (updatedEffects != null) { 31 | _effects = updatedEffects; 32 | } 33 | } 34 | } 35 | 36 | void createAnimation(AnimationInfo animation, TickerProvider vsync) { 37 | final newController = AnimationController(vsync: vsync); 38 | animation.controller = newController; 39 | } 40 | 41 | void setupAnimations(Iterable animations, TickerProvider vsync) { 42 | animations.forEach((animation) => createAnimation(animation, vsync)); 43 | } 44 | 45 | extension AnimatedWidgetExtension on Widget { 46 | Widget animateOnPageLoad( 47 | AnimationInfo animationInfo, { 48 | List? effects, 49 | }) { 50 | animationInfo.maybeUpdateEffects(effects); 51 | return Animate( 52 | effects: animationInfo.effects, 53 | child: this, 54 | onPlay: (controller) => animationInfo.loop ? controller.repeat(reverse: animationInfo.reverse) : null, 55 | onComplete: (controller) => !animationInfo.loop && animationInfo.reverse ? controller.reverse() : null, 56 | ); 57 | } 58 | 59 | Widget animateOnActionTrigger( 60 | AnimationInfo animationInfo, { 61 | List? effects, 62 | bool hasBeenTriggered = false, 63 | }) { 64 | animationInfo.maybeUpdateEffects(effects); 65 | return hasBeenTriggered || animationInfo.applyInitialState 66 | ? Animate(controller: animationInfo.controller, autoPlay: false, effects: animationInfo.effects, child: this) 67 | : this; 68 | } 69 | } 70 | 71 | class TiltEffect extends Effect { 72 | const TiltEffect({ 73 | super.delay, 74 | super.duration, 75 | super.curve, 76 | Offset? begin, 77 | Offset? end, 78 | }) : super( 79 | begin: begin ?? const Offset(0.0, 0.0), 80 | end: end ?? const Offset(0.0, 0.0), 81 | ); 82 | 83 | @override 84 | Widget build( 85 | BuildContext context, 86 | Widget child, 87 | AnimationController controller, 88 | EffectEntry entry, 89 | ) { 90 | Animation animation = buildAnimation(controller, entry); 91 | return getOptimizedBuilder( 92 | animation: animation, 93 | builder: (_, __) => Transform( 94 | transform: Matrix4.identity() 95 | ..setEntry(3, 2, 0.001) 96 | ..rotateX(animation.value.dx) 97 | ..rotateY(animation.value.dy), 98 | alignment: Alignment.center, 99 | child: child, 100 | ), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_autocomplete_options_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:substring_highlight/substring_highlight.dart'; 4 | 5 | /// A widget that displays a list of autocomplete options for a text field. 6 | class AutocompleteOptionsList extends StatelessWidget { 7 | const AutocompleteOptionsList({ 8 | super.key, 9 | required this.textFieldKey, 10 | required this.textController, 11 | required this.options, 12 | required this.onSelected, 13 | required this.textStyle, 14 | this.textAlign = TextAlign.start, 15 | this.optionBackgroundColor, 16 | this.optionHighlightColor, 17 | this.textHighlightStyle, 18 | this.maxHeight, 19 | this.elevation = 4.0, 20 | }); 21 | 22 | /// The key of the text field associated with the autocomplete options list. 23 | final GlobalKey textFieldKey; 24 | 25 | /// The controller for the text field. 26 | final TextEditingController textController; 27 | 28 | /// The list of autocomplete options. 29 | final List options; 30 | 31 | /// The callback function that is called when an option is selected. 32 | final Function(String) onSelected; 33 | 34 | /// The color used to highlight the selected option. 35 | final Color? optionHighlightColor; 36 | 37 | /// The background color of the options. 38 | final Color? optionBackgroundColor; 39 | 40 | /// The style of the text in the options. 41 | final TextStyle textStyle; 42 | 43 | /// The style of the highlighted text in the options. 44 | final TextStyle? textHighlightStyle; 45 | 46 | /// The alignment of the text in the options. 47 | final TextAlign textAlign; 48 | 49 | /// The maximum height of the options list. 50 | final double? maxHeight; 51 | 52 | /// The elevation of the options list. 53 | final double elevation; 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | final textFieldBox = 58 | textFieldKey.currentContext!.findRenderObject() as RenderBox; 59 | final textFieldWidth = textFieldBox.size.width; 60 | return Align( 61 | alignment: Alignment.topLeft, 62 | child: Material( 63 | elevation: elevation, 64 | child: ConstrainedBox( 65 | constraints: BoxConstraints( 66 | maxWidth: textFieldWidth, 67 | maxHeight: maxHeight ?? 200, 68 | ), 69 | child: ListView.builder( 70 | padding: EdgeInsets.zero, 71 | shrinkWrap: true, 72 | itemCount: options.length, 73 | itemBuilder: (context, index) { 74 | final option = options.elementAt(index); 75 | return InkWell( 76 | onTap: () => onSelected(option), 77 | child: Builder(builder: (context) { 78 | final bool highlight = 79 | AutocompleteHighlightedOption.of(context) == index; 80 | if (highlight) { 81 | SchedulerBinding.instance.addPostFrameCallback((timeStamp) { 82 | Scrollable.ensureVisible(context, alignment: 0.5); 83 | }); 84 | } 85 | return Container( 86 | color: highlight 87 | ? optionHighlightColor ?? Theme.of(context).focusColor 88 | : optionBackgroundColor, 89 | padding: const EdgeInsets.all(16.0), 90 | child: SubstringHighlight( 91 | text: option, 92 | term: textController.text, 93 | textStyle: textStyle, 94 | textAlign: textAlign, 95 | textStyleHighlight: textHighlightStyle ?? textStyle, 96 | ), 97 | ); 98 | }), 99 | ); 100 | }, 101 | ), 102 | ), 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_swipeable_stack.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_card_swiper/flutter_card_swiper.dart'; 5 | 6 | /// A widget that displays a stack of swipeable cards. 7 | class FlutterFlowSwipeableStack extends StatefulWidget { 8 | /// Creates a [FlutterFlowSwipeableStack]. 9 | /// 10 | /// - [itemBuilder] is a callback that builds the widget for each card in the stack. 11 | /// - [itemCount] is the total number of cards in the stack. 12 | /// - [controller] is the controller for the swipeable stack. 13 | /// - [onSwipeFn] is a callback that is called when a card is swiped. 14 | /// - [onRightSwipe] is a callback that is called when a card is swiped to the right. 15 | /// - [onLeftSwipe] is a callback that is called when a card is swiped to the left. 16 | /// - [onUpSwipe] is a callback that is called when a card is swiped up. 17 | /// - [onDownSwipe] is a callback that is called when a card is swiped down. 18 | /// - [loop] determines whether the stack should loop back to the beginning when the last card is swiped. 19 | /// - [cardDisplayCount] is the number of cards to display on the stack at a time. 20 | /// - [scale] is the scale factor for the cards in the stack. 21 | /// - [maxAngle] is the maximum rotation angle for the cards in the stack. 22 | /// - [threshold] is the swipe threshold for the cards in the stack. 23 | /// - [cardPadding] is the padding for each card in the stack. 24 | /// - [backCardOffset] is the offset for the back card in the stack. 25 | const FlutterFlowSwipeableStack({ 26 | super.key, 27 | required this.itemBuilder, 28 | required this.itemCount, 29 | required this.controller, 30 | required this.onSwipeFn, 31 | required this.onRightSwipe, 32 | required this.onLeftSwipe, 33 | required this.onUpSwipe, 34 | required this.onDownSwipe, 35 | required this.loop, 36 | required this.cardDisplayCount, 37 | required this.scale, 38 | this.maxAngle, 39 | this.threshold, 40 | this.cardPadding, 41 | this.backCardOffset, 42 | }); 43 | 44 | final Widget Function(BuildContext, int) itemBuilder; 45 | final CardSwiperController controller; 46 | final int itemCount; 47 | final Function(int) onSwipeFn; 48 | final Function(int) onRightSwipe; 49 | final Function(int) onLeftSwipe; 50 | final Function(int) onUpSwipe; 51 | final Function(int) onDownSwipe; 52 | final bool loop; 53 | final int cardDisplayCount; 54 | final double scale; 55 | final double? maxAngle; 56 | final double? threshold; 57 | final EdgeInsetsGeometry? cardPadding; 58 | final Offset? backCardOffset; 59 | 60 | @override 61 | State createState() => _FFSwipeableStackState(); 62 | } 63 | 64 | class _FFSwipeableStackState extends State { 65 | @override 66 | Widget build(BuildContext context) { 67 | return CardSwiper( 68 | controller: widget.controller, 69 | onSwipe: (previousIndex, currentIndex, direction) { 70 | widget.onSwipeFn(previousIndex); 71 | if (direction == CardSwiperDirection.left) { 72 | widget.onLeftSwipe(previousIndex); 73 | } else if (direction == CardSwiperDirection.right) { 74 | widget.onRightSwipe(previousIndex); 75 | } else if (direction == CardSwiperDirection.top) { 76 | widget.onUpSwipe(previousIndex); 77 | } else if (direction == CardSwiperDirection.bottom) { 78 | widget.onDownSwipe(previousIndex); 79 | } 80 | return true; 81 | }, 82 | cardsCount: widget.itemCount, 83 | cardBuilder: (context, index, percentThresholdX, percentThresholdY) { 84 | return widget.itemBuilder(context, index); 85 | }, 86 | isLoop: widget.loop, 87 | maxAngle: widget.maxAngle ?? 30, 88 | threshold: 89 | widget.threshold != null ? (100 * widget.threshold!).round() : 50, 90 | scale: widget.scale, 91 | padding: widget.cardPadding ?? 92 | const EdgeInsets.symmetric(horizontal: 20, vertical: 25), 93 | backCardOffset: widget.backCardOffset ?? const Offset(0, 40), 94 | numberOfCardsDisplayed: min(widget.cardDisplayCount, widget.itemCount), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | analyzer: 3 | errors: 4 | missing_required_param: error 5 | missing_return: warning 6 | todo: ignore 7 | exclude: 8 | - "bin/cache/**" 9 | linter: 10 | rules: 11 | always_declare_return_types: true 12 | always_put_control_body_on_new_line: true 13 | annotate_overrides: true 14 | avoid_bool_literals_in_conditional_expressions: true 15 | avoid_classes_with_only_static_members: true 16 | avoid_empty_else: true 17 | avoid_field_initializers_in_const_classes: true 18 | avoid_function_literals_in_foreach_calls: false 19 | avoid_init_to_null: true 20 | avoid_null_checks_in_equality_operators: true 21 | avoid_relative_lib_imports: true 22 | avoid_renaming_method_parameters: true 23 | avoid_return_types_on_setters: true 24 | avoid_returning_null_for_void: true 25 | avoid_shadowing_type_parameters: true 26 | avoid_slow_async_io: true 27 | avoid_types_as_parameter_names: true 28 | avoid_types_on_closure_parameters: true 29 | avoid_unnecessary_containers: true 30 | avoid_unused_constructor_parameters: true 31 | avoid_void_async: true 32 | await_only_futures: true 33 | camel_case_extensions: true 34 | camel_case_types: true 35 | cancel_subscriptions: true 36 | cascade_invocations: true 37 | close_sinks: true 38 | collection_methods_unrelated_type: true 39 | constant_identifier_names: false 40 | control_flow_in_finally: true 41 | depend_on_referenced_packages: false 42 | directives_ordering: true 43 | empty_catches: true 44 | empty_constructor_bodies: true 45 | empty_statements: true 46 | hash_and_equals: true 47 | implementation_imports: true 48 | library_names: true 49 | library_prefixes: true 50 | library_private_types_in_public_api: true 51 | no_adjacent_strings_in_list: true 52 | no_duplicate_case_values: true 53 | no_leading_underscores_for_local_identifiers: false 54 | overridden_fields: true 55 | package_api_docs: true 56 | package_names: true 57 | package_prefixed_library_names: true 58 | prefer_adjacent_string_concatenation: true 59 | prefer_asserts_in_initializer_lists: true 60 | prefer_collection_literals: true 61 | prefer_conditional_assignment: true 62 | prefer_const_constructors: true 63 | prefer_const_constructors_in_immutables: true 64 | prefer_const_declarations: true 65 | prefer_const_literals_to_create_immutables: true 66 | prefer_contains: true 67 | prefer_final_fields: true 68 | prefer_final_in_for_each: true 69 | prefer_for_elements_to_map_fromIterable: true 70 | prefer_foreach: true 71 | prefer_function_declarations_over_variables: false 72 | prefer_generic_function_type_aliases: true 73 | prefer_if_null_operators: true 74 | prefer_initializing_formals: true 75 | prefer_inlined_adds: true 76 | prefer_interpolation_to_compose_strings: true 77 | prefer_is_empty: true 78 | prefer_is_not_empty: true 79 | prefer_is_not_operator: true 80 | prefer_iterable_whereType: true 81 | prefer_mixin: true 82 | prefer_null_aware_operators: true 83 | prefer_spread_collections: true 84 | prefer_typing_uninitialized_variables: true 85 | prefer_void_to_null: true 86 | recursive_getters: true 87 | slash_for_doc_comments: true 88 | sort_child_properties_last: true 89 | sort_constructors_first: true 90 | sort_pub_dependencies: true 91 | sort_unnamed_constructors_first: true 92 | test_types_in_equals: true 93 | throw_in_finally: true 94 | type_init_formals: true 95 | unawaited_futures: true 96 | unnecessary_await_in_return: true 97 | unnecessary_brace_in_string_interps: true 98 | unnecessary_const: true 99 | unnecessary_getters_setters: true 100 | unnecessary_lambdas: true 101 | unnecessary_new: true 102 | unnecessary_null_aware_assignments: true 103 | unnecessary_null_in_if_null_operators: true 104 | unnecessary_overrides: true 105 | unnecessary_parenthesis: true 106 | unnecessary_statements: true 107 | unnecessary_this: true 108 | unrelated_type_equality_checks: true 109 | use_build_context_synchronously: false 110 | use_full_hex_values_for_flutter_colors: true 111 | use_function_type_syntax_for_parameters: true 112 | use_rethrow_when_possible: true 113 | use_to_and_as_if_applicable: true 114 | valid_regexps: true 115 | void_checks: true 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlutterFlow UI 2 | 3 | `flutterflow_ui` simplifies the process of adding FlutterFlow generated UI code to your Flutter projects. It streamlines integration, saving you time and effort in the UI development for your Flutter app. 4 | 5 | ## Generate code in your FlutterFlow project 6 | 7 | In your FlutterFlow project, navigate to the code icon and click on "View Code". 8 | 9 | 10 | 11 | Here, you will find the FlutterFlow-generated code for your pages and components. Choose the specific page or component you need, then copy the widget code. Paste this code into a new Flutter file within your Flutter project. 12 | 13 | Ensure you also include the generated model code in the same file or in a separate file, depending on your directory structure. In some cases, this file may initially be empty, and you can decide whether to keep or remove it later. 14 | 15 | After pasting the code, you might encounter some errors, but don't worry. These issues will be resolved through the following steps. 16 | 17 | 18 | ## Add Dependency 19 | 20 | Now in your Flutter project, open your `pubspec.yaml` file and add `flutterflow_ui` under dependencies: 21 | 22 | ```yaml 23 | dependencies: 24 | flutterflow_ui: 25 | ``` 26 | Remember to run `flutter pub get` 27 | 28 | ## Replace the `flutter_flow` dependencies with the package import 29 | 30 | In your imports, you will see a bunch of `flutter_flow/flutter_flow...` imports that are usually present in a FlutterFlow project but with this package you can resolve these errors. 31 | 32 | Remove such imports: 33 | ```dart 34 | import '/flutter_flow/flutter_flow_animations.dart'; 35 | import '/flutter_flow/flutter_flow_icon_button.dart'; 36 | import '/flutter_flow/flutter_flow_theme.dart'; 37 | import '/flutter_flow/flutter_flow_util.dart'; 38 | ``` 39 | 40 | And replace it with the package import: 41 | 42 | ```dart 43 | import 'package:flutterflow_ui/flutterflow_ui.dart'; 44 | ``` 45 | 46 | ## Cleaning up unnecessary code 47 | 48 | In the beginning of the build method, you might encounter the line `context.watch();`. This line is beneficial in a FlutterFlow project, but in your Flutter project, you might have a different method for managing global constants and variables. If that's the case, feel free to remove this line of code. 49 | 50 | Additionally, if you're not using the Provider package for state management in your project, you can safely remove the import statement related to it. 51 | 52 | Lastly, double-check that your model file, if it's located in a separate file, is correctly imported. 53 | 54 | With these adjustments, you're ready to run the FlutterFlow-generated code in your Flutter project. 55 | __________________________________________ 56 | 57 | 58 | ## Some usecases 59 | 60 | ### How to add a widget with animation to an existing Flutter screen? 61 | 62 | * Begin by right-clicking on the component or widget within your FlutterFlow canvas. Then, select "Copy Widget Code." 63 | 64 | 65 | 66 | Alternatively, you can follow similar steps as mentioned above, but click on "View Code" from the Developer Menu. After that, click on the widget in the preview that you want to copy. The code will be displayed on the left-hand side. 67 | 68 | * Next, paste the widget code into your Flutter widget file wherever you'd like to place it. 69 | * If you encounter errors related to `animationMap`, don't worry. This is located in your Stateful Widget of the screen where it's currently placed. You can now copy the `animationsMap` to your widget body. Once you've done this, the errors will disappear, and you can run your code without any issues. 70 | 71 | 72 | ## Supports the following FlutterFlow widgets 73 | 74 | * Layout Elements supported by Material/Cupertino package 75 | * Ad Banner 76 | * Audio Player 77 | * Calendar 78 | * Charts 79 | * Checkbox Group 80 | * Choice Chips 81 | * Counter Button 82 | * Credit Card 83 | * Data Table 84 | * Drop Down 85 | * Expandable Image & Circle Image 86 | * Google Map 87 | * Icon Button 88 | * Language Selector 89 | * Media Display 90 | * Mux Broadcast 91 | * Radio Button 92 | * Rive 93 | * Static Map 94 | * Swipeable Stack 95 | * Timer 96 | * Toggle Icon 97 | * Tab Bar 98 | * Web View 99 | 100 | ## Documentation & more usages 101 | You can check out our [documentation](https://docs.flutterflow.io/flutter/export-flutterflow-ui-code-to-your-flutter-project) for more examples. 102 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_static_map.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutterflow_ui/src/utils/lat_lng.dart'; 4 | import 'package:mapbox_search/mapbox_search.dart' as mapbox; 5 | 6 | /// A widget that displays a static map using the Mapbox API. 7 | class FlutterFlowStaticMap extends StatelessWidget { 8 | const FlutterFlowStaticMap({ 9 | super.key, 10 | required this.location, 11 | required this.apiKey, 12 | required this.style, 13 | required this.width, 14 | required this.height, 15 | this.fit, 16 | this.borderRadius = BorderRadius.zero, 17 | this.markerColor, 18 | this.markerUrl, 19 | this.cached = false, 20 | this.zoom = 12, 21 | this.tilt = 0, 22 | this.rotation = 0, 23 | }); 24 | 25 | /// The location to display on the map. 26 | final LatLng location; 27 | 28 | /// The API key for accessing the Mapbox API. 29 | final String apiKey; 30 | 31 | /// The style of the map. 32 | final mapbox.MapBoxStyle style; 33 | 34 | /// The width of the map widget. 35 | final double width; 36 | 37 | /// The height of the map widget. 38 | final double height; 39 | 40 | /// How the map should be inscribed into the available space. 41 | final BoxFit? fit; 42 | 43 | /// The border radius of the map widget. 44 | final BorderRadius borderRadius; 45 | 46 | /// The color of the marker on the map. 47 | final Color? markerColor; 48 | 49 | /// The URL of the custom marker icon. 50 | final String? markerUrl; 51 | 52 | /// Whether to cache the map image. 53 | final bool cached; 54 | 55 | /// The zoom level of the map. 56 | final int zoom; 57 | 58 | /// The tilt angle of the map camera. 59 | final int tilt; 60 | 61 | /// The rotation angle of the map camera. 62 | final int rotation; 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | final imageWidth = width.clamp(1, 1280).toInt(); 67 | final imageHeight = height.clamp(1, 1280).toInt(); 68 | final imagePath = getStaticMapImageURL(location, apiKey, style, imageWidth, 69 | imageHeight, markerColor, markerUrl, zoom, rotation, tilt); 70 | return ClipRRect( 71 | borderRadius: borderRadius, 72 | child: cached 73 | ? CachedNetworkImage( 74 | imageUrl: imagePath, 75 | width: width, 76 | height: height, 77 | fit: fit, 78 | ) 79 | : Image.network( 80 | imagePath, 81 | width: width, 82 | height: height, 83 | fit: fit, 84 | ), 85 | ); 86 | } 87 | } 88 | 89 | String getStaticMapImageURL( 90 | LatLng location, 91 | String apiKey, 92 | mapbox.MapBoxStyle mapStyle, 93 | int width, 94 | int height, 95 | Color? markerColor, 96 | String? markerURL, 97 | int zoom, 98 | int rotation, 99 | int tilt, 100 | ) { 101 | final finalLocation = ( 102 | lat: location.latitude.clamp(-90, 90).toDouble(), 103 | long: location.longitude.clamp(-180, 180).toDouble(), 104 | ); 105 | final finalRotation = rotation.clamp(-180, 180).round(); 106 | final finalTilt = tilt.clamp(0, 60).round(); 107 | final finalZoom = zoom.clamp(0, 22).round(); 108 | final image = mapbox.StaticImage(apiKey: apiKey); 109 | if (markerColor == null && (markerURL == null || markerURL.trim().isEmpty)) { 110 | return image 111 | .getStaticUrlWithoutMarker( 112 | center: finalLocation, 113 | style: mapStyle, 114 | width: width.round(), 115 | height: height.round(), 116 | zoomLevel: finalZoom, 117 | bearing: finalRotation, 118 | pitch: finalTilt, 119 | ) 120 | .toString(); 121 | } else { 122 | return image 123 | .getStaticUrlWithMarker( 124 | marker: markerURL == null || markerURL.trim().isEmpty 125 | ? mapbox.MapBoxMarker( 126 | markerColor: mapbox.RgbColor( 127 | markerColor!.red, 128 | markerColor.green, 129 | markerColor.blue, 130 | ), 131 | markerLetter: mapbox.MakiIcons.circle.value, 132 | markerSize: mapbox.MarkerSize.MEDIUM, 133 | ) 134 | : null, 135 | markerUrl: markerURL, 136 | center: finalLocation, 137 | style: mapStyle, 138 | width: width.round(), 139 | height: height.round(), 140 | zoomLevel: finalZoom, 141 | bearing: finalRotation, 142 | pitch: finalTilt, 143 | ) 144 | .toString(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_ad_banner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart' show kIsWeb; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/scheduler.dart'; 6 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 7 | 8 | /// A widget that displays a banner ad. 9 | class FlutterFlowAdBanner extends StatefulWidget { 10 | const FlutterFlowAdBanner({ 11 | super.key, 12 | this.width, 13 | this.height, 14 | required this.showsTestAd, 15 | this.iOSAdUnitID, 16 | this.androidAdUnitID, 17 | }); 18 | 19 | /// The width of the ad banner. 20 | final double? width; 21 | 22 | /// The height of the ad banner. 23 | final double? height; 24 | 25 | /// Whether to show a test ad. 26 | final bool showsTestAd; 27 | 28 | /// The Ad Unit ID for iOS. 29 | final String? iOSAdUnitID; 30 | 31 | /// The Ad Unit ID for Android. 32 | final String? androidAdUnitID; 33 | 34 | @override 35 | State createState() => _FlutterFlowAdBannerState(); 36 | } 37 | 38 | class _FlutterFlowAdBannerState extends State { 39 | static const AdRequest request = AdRequest(); 40 | 41 | BannerAd? _anchoredBanner; 42 | AdWidget? adWidget; 43 | 44 | @override 45 | void initState() { 46 | super.initState(); 47 | 48 | SchedulerBinding.instance.addPostFrameCallback((_) { 49 | _createAnchoredBanner(context); 50 | }); 51 | } 52 | 53 | @override 54 | void dispose() { 55 | super.dispose(); 56 | _anchoredBanner?.dispose(); 57 | } 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | var loadingText = 'Ad Loading... \\n\\n'; 62 | if (widget.showsTestAd) { 63 | loadingText += 64 | 'If this takes a long time, you may have to check whether the ad is ' 65 | 'being covered from a parent widget. For example, a larger width than ' 66 | 'the device screen size or a large border radius encompassing the ad banner ' 67 | 'may stop ads from loading.\\n\\n' 68 | 'If a full-width banner is desired for your app, leave the width and ' 69 | 'height of the AdBanner widget empty. AdBanner will automatically' 70 | 'match the size of the banner to the device screen.'; 71 | } 72 | 73 | return _anchoredBanner != null && adWidget != null 74 | ? Container( 75 | alignment: Alignment.center, 76 | color: Colors.red, 77 | width: _anchoredBanner!.size.width.toDouble(), 78 | height: _anchoredBanner!.size.height.toDouble(), 79 | child: adWidget, 80 | ) 81 | : Container( 82 | color: Colors.black, 83 | alignment: Alignment.center, 84 | child: Padding( 85 | padding: const EdgeInsets.all(8.0), 86 | child: Text( 87 | loadingText, 88 | style: const TextStyle( 89 | fontSize: 10.0, 90 | color: Colors.white, 91 | ), 92 | ), 93 | ), 94 | ); 95 | } 96 | 97 | /// Creates an anchored banner ad. 98 | Future _createAnchoredBanner(BuildContext context) async { 99 | final AdSize? size = widget.width != null && widget.height != null 100 | ? AdSize( 101 | height: widget.height!.toInt(), 102 | width: widget.width!.toInt(), 103 | ) 104 | : await AdSize.getAnchoredAdaptiveBannerAdSize( 105 | widget.width == null ? Orientation.portrait : Orientation.landscape, 106 | widget.width == null 107 | ? MediaQuery.sizeOf(context).width.truncate() 108 | : MediaQuery.sizeOf(context).height.truncate(), 109 | ); 110 | 111 | if (size == null) { 112 | print('Unable to get size of anchored banner.'); 113 | return; 114 | } 115 | 116 | final isAndroid = !kIsWeb && Platform.isAndroid; 117 | final BannerAd banner = BannerAd( 118 | size: size, 119 | request: request, 120 | adUnitId: widget.showsTestAd 121 | ? isAndroid 122 | ? 'ca-app-pub-3940256099942544/6300978111' 123 | : 'ca-app-pub-3940256099942544/2934735716' 124 | : isAndroid 125 | ? widget.androidAdUnitID! 126 | : widget.iOSAdUnitID!, 127 | listener: BannerAdListener( 128 | onAdLoaded: (ad) { 129 | print('\$BannerAd loaded.'); 130 | if (mounted) { 131 | setState(() => _anchoredBanner = ad as BannerAd); 132 | } 133 | }, 134 | onAdFailedToLoad: (ad, error) { 135 | print('\$BannerAd failedToLoad: \$error'); 136 | ad.dispose(); 137 | }, 138 | onAdOpened: (ad) => print('\$BannerAd onAdOpened.'), 139 | onAdClosed: (ad) => print('\$BannerAd onAdClosed.'), 140 | ), 141 | ); 142 | await banner.load(); 143 | 144 | adWidget = AdWidget(ad: banner); 145 | setState(() {}); 146 | return; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_timer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:stop_watch_timer/stop_watch_timer.dart'; 5 | 6 | // Simple wrapper around StopWatchTimer that emits notifications on events. 7 | class FlutterFlowTimerController with ChangeNotifier { 8 | FlutterFlowTimerController(this.timer); 9 | final StopWatchTimer timer; 10 | 11 | void onStartTimer() { 12 | timer.onStartTimer(); 13 | notifyListeners(); 14 | } 15 | 16 | void onStopTimer() { 17 | timer.onStopTimer(); 18 | notifyListeners(); 19 | } 20 | 21 | void onResetTimer() { 22 | timer.onResetTimer(); 23 | late final StreamSubscription subscription; 24 | // We can't notify listeners right away: they'll see the old timer value. 25 | // We need to wait until the next time is emitted. 26 | subscription = timer.rawTime.listen((_) { 27 | notifyListeners(); 28 | subscription.cancel(); 29 | }); 30 | } 31 | 32 | @override 33 | void dispose() { 34 | timer.dispose(); 35 | super.dispose(); 36 | } 37 | } 38 | 39 | /// A timer widget that displays and manages time. 40 | class FlutterFlowTimer extends StatefulWidget { 41 | /// Creates a [FlutterFlowTimer] widget. 42 | const FlutterFlowTimer({ 43 | super.key, 44 | required this.initialTime, 45 | required this.controller, 46 | required this.getDisplayTime, 47 | required this.onChanged, 48 | this.updateStateInterval, 49 | this.onEnded, 50 | required this.textAlign, 51 | required this.style, 52 | }); 53 | 54 | /// The initial time for the timer. 55 | final int initialTime; 56 | 57 | /// The controller for the timer. 58 | final FlutterFlowTimerController controller; 59 | 60 | /// A function that returns the formatted display time. 61 | final String Function(int) getDisplayTime; 62 | 63 | /// A callback function that is called when the timer value changes. 64 | final Function(int value, String displayTime, bool shouldUpdate) onChanged; 65 | 66 | /// The interval at which the timer state should be updated. 67 | final Duration? updateStateInterval; 68 | 69 | /// A callback function that is called when the timer ends. 70 | final Function()? onEnded; 71 | 72 | /// The alignment of the timer text. 73 | final TextAlign textAlign; 74 | 75 | /// The style of the timer text. 76 | final TextStyle style; 77 | 78 | @override 79 | State createState() => _FlutterFlowTimerState(); 80 | } 81 | 82 | class _FlutterFlowTimerState extends State { 83 | int get timerValue => widget.controller.timer.rawTime.value; 84 | bool get isCountUp => widget.controller.timer.mode == StopWatchMode.countUp; 85 | 86 | late String _displayTime; 87 | late int lastUpdateMs; 88 | 89 | Function() get onEnded => widget.onEnded ?? () {}; 90 | 91 | void _initTimer({required bool shouldUpdate}) { 92 | // Initialize timer display time and last update time. 93 | _displayTime = widget.getDisplayTime(widget.controller.timer.rawTime.value); 94 | lastUpdateMs = timerValue; 95 | // Update timer value and display time. 96 | widget.onChanged(timerValue, _displayTime, shouldUpdate); 97 | } 98 | 99 | @override 100 | void initState() { 101 | super.initState(); 102 | // Set the initial time. 103 | widget.controller.timer.setPresetTime(mSec: widget.initialTime, add: false); 104 | // Initialize timer properties without updating outer state. 105 | _initTimer(shouldUpdate: false); 106 | // Add a listener for when the timer value changes to update the 107 | // displayed timer value. 108 | widget.controller.timer.rawTime.listen((_) { 109 | _displayTime = widget.getDisplayTime(timerValue); 110 | widget.onChanged(timerValue, _displayTime, _shouldUpdate()); 111 | if (mounted) { 112 | setState(() {}); 113 | } 114 | }); 115 | // Add listener for actions executed on timer. 116 | widget.controller.addListener(() => _initTimer(shouldUpdate: true)); 117 | 118 | // Add listener for when the timer ends. 119 | widget.controller.timer.fetchEnded.listen((_) => onEnded()); 120 | } 121 | 122 | bool _shouldUpdate() { 123 | // If a null or 0ms update interval is provided, always update. 124 | final updateIntervalMs = widget.updateStateInterval?.inMilliseconds; 125 | if (updateIntervalMs == null || updateIntervalMs == 0) { 126 | return true; 127 | } 128 | // Otherwise, we only update after the specified duration has passed 129 | // since the most recent update. 130 | final cutoff = lastUpdateMs + updateIntervalMs * (isCountUp ? 1 : -1); 131 | final shouldUpdate = isCountUp ? timerValue > cutoff : timerValue < cutoff; 132 | if (shouldUpdate) { 133 | lastUpdateMs = timerValue; 134 | } 135 | return shouldUpdate; 136 | } 137 | 138 | @override 139 | Widget build(BuildContext context) => Text( 140 | _displayTime, 141 | textAlign: widget.textAlign, 142 | style: widget.style, 143 | ); 144 | } 145 | -------------------------------------------------------------------------------- /lib/src/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Constants that we should use throughout the app for widget dimension/stylings. 4 | 5 | /// Dimensions 6 | 7 | const double kPadding2px = 2.0; 8 | const double kPadding4px = 4.0; 9 | const double kPadding8px = 8.0; 10 | const double kPadding12px = 12.0; 11 | const double kPadding16px = 16.0; 12 | const double kPadding20px = 20.0; 13 | const double kPadding24px = 24.0; 14 | const double kPadding28px = 28.0; 15 | 16 | const double kBaseSize8px = 8.0; 17 | const double kBaseSize12px = 12.0; 18 | const double kBaseSize16px = 16.0; 19 | const double kBaseSize20px = 20.0; 20 | const double kBaseSize24px = 24.0; 21 | const double kBaseSize28px = 28.0; 22 | const double kBaseSize32px = 32.0; 23 | const double kBaseSize36px = 36.0; 24 | const double kBaseSize48px = 48.0; 25 | const double kBaseSize60px = 60.0; 26 | const double kBaseSize72px = 72.0; 27 | const double kBaseSize96px = 96.0; 28 | const double kBaseSize120px = 120.0; 29 | const double kBaseSize160px = 160.0; 30 | 31 | const double kWidth48px = 48.0; 32 | const double kWidth56px = 56.0; 33 | //* Width for a property editor that is a quarter of the width of panel. 34 | const double kWidth60px = 60.0; 35 | const double kWidth64px = 64.0; 36 | const double kWidth72px = 72.0; 37 | const double kWidth80px = 80.0; 38 | const double kWidth96px = 96.0; 39 | const double kWidth108px = 108.0; 40 | const double kWidth120px = 120.0; 41 | //* Width for a property editor that is half the width of panel. 42 | const double kWidth132px = 132.0; 43 | const double kWidth144px = 144.0; 44 | const double kWidth160px = 160.0; 45 | //* Width for a property editor that is total the width of panel. 46 | const double kWidth276px = 276.0; 47 | 48 | const double kWidth440px = 440.0; 49 | const double kWidth480px = 480.0; 50 | const double kWidth600px = 600.0; 51 | const double kWidth720px = 720.0; 52 | const double kWidth840px = 840.0; 53 | 54 | const double kHeight16px = 16.0; 55 | const double kHeight20px = 20.0; 56 | const double kHeight24px = 24.0; 57 | const double kHeight28px = 28.0; 58 | //* Typically used for input fields' height. 59 | const double kHeight32px = 32.0; 60 | const double kHeight36px = 36.0; 61 | const double kHeight40px = 40.0; 62 | const double kHeight48px = 48.0; 63 | const double kHeight56px = 56.0; 64 | const double kHeight64px = 64.0; 65 | const double kHeight72px = 72.0; 66 | const double kHeight80px = 80.0; 67 | const double kHeight88px = 88.0; 68 | const double kHeight96px = 96.0; 69 | 70 | const double kHeight240px = 240.0; 71 | const double kHeight320px = 320.0; 72 | const double kHeight400px = 400.0; 73 | const double kHeight480px = 480.0; 74 | 75 | /// Border Radius 76 | 77 | const double kBorderRadius4px = 4.0; 78 | const double kBorderRadius8px = 8.0; 79 | const double kBorderRadius12px = 12.0; 80 | 81 | /// Border Width 82 | 83 | const double kBorderWidth1px = 1.0; 84 | const double kBorderWidth1_5px = 1.5; 85 | const double kBorderWidth2px = 2.0; 86 | 87 | /// Line Width 88 | 89 | const double kLineWidth1px = 1.0; 90 | const double kLineWidth2px = 2.0; 91 | const double kLineWidth4px = 4.0; 92 | 93 | //* Typically used for icon size in property name line. 94 | const double kIconSize14px = 14.0; 95 | const double kIconSize16px = 16.0; 96 | const double kIconSize20px = 20.0; 97 | const double kIconSize22px = 22.0; 98 | const double kIconSize24px = 24.0; 99 | const double kIconSize32px = 32.0; 100 | 101 | /// Durations 102 | 103 | const Duration kDuration250ms = Duration(milliseconds: 250); 104 | const Duration kDuration500ms = Duration(milliseconds: 500); 105 | 106 | /// Durations 107 | 108 | const Curve kCurveEase = Curves.ease; 109 | 110 | /// Elevations and shadowing 111 | 112 | const double kElevation0 = 0.0; 113 | const double kElevation2 = 2.0; 114 | const double kElevation4 = 4.0; 115 | const double kElevation8 = 8.0; 116 | 117 | final kBoxShadow2 = kElevationToShadow[2]!; 118 | final kBoxShadow4 = kElevationToShadow[4]!; 119 | final kBoxShadow8 = kElevationToShadow[8]!; 120 | 121 | /// Blur 122 | 123 | const double kBlur4 = 4.0; 124 | const double kBlur8 = 8.0; 125 | 126 | /// Opacity 127 | 128 | const double kOpacity0_2 = 0.2; 129 | const double kOpacity0_4 = 0.4; 130 | const double kOpacity0_6 = 0.6; 131 | const double kOpacity0_8 = 0.8; 132 | 133 | /// Color 134 | 135 | const kErrorColor = Color(0xFFDF3F3F); 136 | const kPrimaryColor = Color(0xFF4B39EF); 137 | const kSecondaryColor = Color(0xFFEE8B60); 138 | const kTertiaryColor = Color(0xFF1D2429); 139 | const kSuccessToastColor = Color(0xFF39D2C0); 140 | const kGrey250 = Color(0xFFE3E7ED); 141 | const kGrey800 = Color(0xFF424E5A); 142 | const kDiffAddedColor = Colors.green; 143 | const kDiffDeletedColor = Color.fromARGB(255, 255, 129, 129); 144 | 145 | /// Font Sizes 146 | 147 | const double kFontSize12px = 12.0; 148 | const double kFontSize13px = 13.0; 149 | const double kFontSize14px = 14.0; 150 | const double kFontSize16px = 16.0; 151 | const double kFontSize18px = 18.0; 152 | const double kFontSize20px = 20.0; 153 | const double kFontSize24px = 24.0; 154 | const double kFontSize32px = 32.0; 155 | const double kFontSize36px = 36.0; 156 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_web_view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_picker/file_picker.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/gestures.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutterflow_ui/src/utils/flutter_flow_helpers.dart'; 8 | import 'package:url_launcher/url_launcher.dart'; 9 | import 'package:webview_flutter/webview_flutter.dart' hide NavigationDecision; 10 | import 'package:webview_flutter_android/webview_flutter_android.dart'; 11 | import 'package:webviewx_plus/webviewx_plus.dart'; 12 | 13 | /// A widget that displays web content in a WebView. 14 | class FlutterFlowWebView extends StatefulWidget { 15 | /// Creates a [FlutterFlowWebView] widget. 16 | /// 17 | /// - [content] parameter specifies the web content to be displayed. 18 | /// - [width] and [height] parameters specify the dimensions of the WebView. 19 | /// - [bypass] parameter determines whether to bypass the WebView and open the content in the default browser. 20 | /// - [horizontalScroll] parameter determines whether to enable horizontal scrolling in the WebView. 21 | /// - [verticalScroll] parameter determines whether to enable vertical scrolling in the WebView. 22 | /// - [html] parameter determines whether the content is HTML. 23 | const FlutterFlowWebView({ 24 | super.key, 25 | required this.content, 26 | this.width, 27 | this.height, 28 | this.bypass = false, 29 | this.horizontalScroll = false, 30 | this.verticalScroll = false, 31 | this.html = false, 32 | }); 33 | 34 | /// The web content to be displayed in the WebView. 35 | final String content; 36 | 37 | /// The width of the WebView. 38 | final double? width; 39 | 40 | /// The height of the WebView. 41 | final double? height; 42 | 43 | /// Determines whether to bypass the WebView and open the content in the default browser. 44 | final bool bypass; 45 | 46 | /// Determines whether to enable horizontal scrolling in the WebView. 47 | final bool horizontalScroll; 48 | 49 | /// Determines whether to enable vertical scrolling in the WebView. 50 | final bool verticalScroll; 51 | 52 | /// Determines whether the content is HTML. 53 | final bool html; 54 | 55 | @override 56 | State createState() => _FlutterFlowWebViewState(); 57 | } 58 | 59 | class _FlutterFlowWebViewState extends State { 60 | @override 61 | Widget build(BuildContext context) => WebViewX( 62 | key: webviewKey, 63 | width: widget.width ?? MediaQuery.sizeOf(context).width, 64 | height: widget.height ?? MediaQuery.sizeOf(context).height, 65 | ignoreAllGestures: false, 66 | initialContent: widget.content, 67 | initialMediaPlaybackPolicy: 68 | AutoMediaPlaybackPolicy.requireUserActionForAllMediaTypes, 69 | initialSourceType: widget.html 70 | ? SourceType.html 71 | : widget.bypass 72 | ? SourceType.urlBypass 73 | : SourceType.url, 74 | javascriptMode: JavascriptMode.unrestricted, 75 | onWebViewCreated: (controller) async { 76 | if (controller.connector is WebViewController && isAndroid) { 77 | final androidController = 78 | controller.connector.platform as AndroidWebViewController; 79 | await androidController.setOnShowFileSelector(_androidFilePicker); 80 | } 81 | }, 82 | navigationDelegate: (request) async { 83 | if (isAndroid) { 84 | if (request.content.source 85 | .startsWith('https://api.whatsapp.com/send?phone')) { 86 | String url = request.content.source; 87 | 88 | await launchUrl( 89 | Uri.parse(url), 90 | mode: LaunchMode.externalApplication, 91 | ); 92 | return NavigationDecision.prevent; 93 | } 94 | } 95 | return NavigationDecision.navigate; 96 | }, 97 | webSpecificParams: const WebSpecificParams( 98 | webAllowFullscreenContent: true, 99 | ), 100 | mobileSpecificParams: MobileSpecificParams( 101 | debuggingEnabled: false, 102 | gestureNavigationEnabled: true, 103 | mobileGestureRecognizers: { 104 | if (widget.verticalScroll) 105 | const Factory( 106 | VerticalDragGestureRecognizer.new, 107 | ), 108 | if (widget.horizontalScroll) 109 | const Factory( 110 | HorizontalDragGestureRecognizer.new, 111 | ), 112 | }, 113 | androidEnableHybridComposition: true, 114 | ), 115 | ); 116 | 117 | Key get webviewKey => Key( 118 | [ 119 | widget.content, 120 | widget.width, 121 | widget.height, 122 | widget.bypass, 123 | widget.horizontalScroll, 124 | widget.verticalScroll, 125 | widget.html, 126 | ].map((s) => s?.toString() ?? '').join(), 127 | ); 128 | 129 | Future> _androidFilePicker( 130 | final FileSelectorParams params, 131 | ) async { 132 | final result = await FilePicker.platform.pickFiles(); 133 | 134 | if (result != null && result.files.single.path != null) { 135 | final file = File(result.files.single.path!); 136 | return [file.uri.toString()]; 137 | } 138 | return []; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_choice_chips.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:flutterflow_ui/src/utils/flutter_flow_helpers.dart'; 4 | import 'package:flutterflow_ui/src/utils/form_field_controller.dart'; 5 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 6 | 7 | class ChipData { 8 | const ChipData(this.label, [this.iconData]); 9 | final String label; 10 | final IconData? iconData; 11 | } 12 | 13 | class ChipStyle { 14 | const ChipStyle({ 15 | this.backgroundColor, 16 | this.textStyle, 17 | this.iconColor, 18 | this.iconSize, 19 | this.labelPadding, 20 | this.elevation, 21 | this.borderColor, 22 | this.borderWidth, 23 | this.borderRadius, 24 | }); 25 | final Color? backgroundColor; 26 | final TextStyle? textStyle; 27 | final Color? iconColor; 28 | final double? iconSize; 29 | final EdgeInsetsGeometry? labelPadding; 30 | final double? elevation; 31 | final Color? borderColor; 32 | final double? borderWidth; 33 | final BorderRadius? borderRadius; 34 | } 35 | 36 | class FlutterFlowChoiceChips extends StatefulWidget { 37 | const FlutterFlowChoiceChips({ 38 | super.key, 39 | required this.options, 40 | required this.onChanged, 41 | required this.controller, 42 | required this.selectedChipStyle, 43 | required this.unselectedChipStyle, 44 | required this.chipSpacing, 45 | this.rowSpacing = 0.0, 46 | required this.multiselect, 47 | this.initialized = true, 48 | this.alignment = WrapAlignment.start, 49 | this.disabledColor, 50 | this.wrapped = true, 51 | }); 52 | 53 | final List options; 54 | final void Function(List?)? onChanged; 55 | final FormFieldController> controller; 56 | final ChipStyle selectedChipStyle; 57 | final ChipStyle unselectedChipStyle; 58 | final double chipSpacing; 59 | final double rowSpacing; 60 | final bool multiselect; 61 | final bool initialized; 62 | final WrapAlignment alignment; 63 | final Color? disabledColor; 64 | final bool wrapped; 65 | 66 | @override 67 | State createState() => _FlutterFlowChoiceChipsState(); 68 | } 69 | 70 | class _FlutterFlowChoiceChipsState extends State { 71 | late List choiceChipValues; 72 | List get selectedValues => widget.controller.value ?? []; 73 | 74 | @override 75 | void initState() { 76 | super.initState(); 77 | choiceChipValues = List.from(selectedValues); 78 | if (!widget.initialized && choiceChipValues.isNotEmpty) { 79 | SchedulerBinding.instance.addPostFrameCallback( 80 | (_) { 81 | if (widget.onChanged != null) { 82 | widget.onChanged!(choiceChipValues); 83 | } 84 | }, 85 | ); 86 | } 87 | } 88 | 89 | @override 90 | void dispose() { 91 | super.dispose(); 92 | } 93 | 94 | @override 95 | Widget build(BuildContext context) { 96 | final children = widget.options.map( 97 | (option) { 98 | final selected = selectedValues.contains(option.label); 99 | final style = 100 | selected ? widget.selectedChipStyle : widget.unselectedChipStyle; 101 | return Theme( 102 | data: Theme.of(context).copyWith(canvasColor: Colors.transparent), 103 | child: ChoiceChip( 104 | selected: selected, 105 | onSelected: widget.onChanged != null 106 | ? (isSelected) { 107 | choiceChipValues = List.from(selectedValues); 108 | if (isSelected) { 109 | widget.multiselect 110 | ? choiceChipValues.add(option.label) 111 | : choiceChipValues = [option.label]; 112 | widget.controller.value = List.from(choiceChipValues); 113 | setState(() {}); 114 | } else { 115 | if (widget.multiselect) { 116 | choiceChipValues.remove(option.label); 117 | widget.controller.value = List.from(choiceChipValues); 118 | setState(() {}); 119 | } 120 | } 121 | widget.onChanged!(choiceChipValues); 122 | } 123 | : null, 124 | label: Text( 125 | option.label, 126 | style: style.textStyle, 127 | ), 128 | labelPadding: style.labelPadding, 129 | avatar: option.iconData != null 130 | ? FaIcon( 131 | option.iconData, 132 | size: style.iconSize, 133 | color: style.iconColor, 134 | ) 135 | : null, 136 | elevation: style.elevation, 137 | disabledColor: widget.disabledColor, 138 | selectedColor: 139 | selected ? widget.selectedChipStyle.backgroundColor : null, 140 | backgroundColor: 141 | selected ? null : widget.unselectedChipStyle.backgroundColor, 142 | shape: RoundedRectangleBorder( 143 | borderRadius: style.borderRadius ?? BorderRadius.circular(16), 144 | side: BorderSide( 145 | color: style.borderColor ?? Colors.transparent, 146 | width: style.borderWidth ?? 0, 147 | ), 148 | ), 149 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 150 | ), 151 | ); 152 | }, 153 | ).toList(); 154 | 155 | if (widget.wrapped) { 156 | return Wrap( 157 | spacing: widget.chipSpacing, 158 | runSpacing: widget.rowSpacing, 159 | alignment: widget.alignment, 160 | crossAxisAlignment: WrapCrossAlignment.center, 161 | children: children, 162 | ); 163 | } else { 164 | return SingleChildScrollView( 165 | scrollDirection: Axis.horizontal, 166 | clipBehavior: Clip.none, 167 | child: Row( 168 | crossAxisAlignment: CrossAxisAlignment.center, 169 | children: children.divide( 170 | SizedBox(width: widget.chipSpacing), 171 | ), 172 | ), 173 | ); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_checkbox_group.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/scheduler.dart'; 4 | import 'package:flutterflow_ui/src/utils/form_field_controller.dart'; 5 | 6 | /// A group of checkboxes that allows the user to select multiple options. 7 | class FlutterFlowCheckboxGroup extends StatefulWidget { 8 | /// Creates a [FlutterFlowCheckboxGroup]. 9 | /// 10 | /// - [options] parameter is a list of strings representing the available options. 11 | /// - [onChanged] parameter is a callback function that is called when the selection changes. 12 | /// - [controller] parameter is a controller for the form field that holds the selected options. 13 | /// - [textStyle] parameter is the style of the text for the checkboxes. 14 | /// - [labelPadding] parameter is the padding around the checkbox labels. 15 | /// - [itemPadding] parameter is the padding around each checkbox item. 16 | /// - [activeColor] parameter is the color of the checkbox when it is selected. 17 | /// - [checkColor] parameter is the color of the check mark inside the checkbox. 18 | /// - [checkboxBorderRadius] parameter is the border radius of the checkbox. 19 | /// - [checkboxBorderColor] parameter is the color of the checkbox border. 20 | /// - [initialized] parameter indicates whether the checkbox group is initialized with a value. 21 | /// - [unselectedTextStyle] parameter is the style of the text for unselected checkboxes. 22 | const FlutterFlowCheckboxGroup({ 23 | super.key, 24 | required this.options, 25 | required this.onChanged, 26 | required this.controller, 27 | required this.textStyle, 28 | this.labelPadding, 29 | this.itemPadding, 30 | required this.activeColor, 31 | required this.checkColor, 32 | this.checkboxBorderRadius, 33 | required this.checkboxBorderColor, 34 | this.initialized = true, 35 | this.unselectedTextStyle, 36 | }); 37 | 38 | final List options; 39 | final void Function(List)? onChanged; 40 | final FormFieldController> controller; 41 | final TextStyle textStyle; 42 | final EdgeInsetsGeometry? labelPadding; 43 | final EdgeInsetsGeometry? itemPadding; 44 | final Color activeColor; 45 | final Color checkColor; 46 | final BorderRadius? checkboxBorderRadius; 47 | final Color checkboxBorderColor; 48 | final bool initialized; 49 | final TextStyle? unselectedTextStyle; 50 | 51 | @override 52 | State createState() => 53 | _FlutterFlowCheckboxGroupState(); 54 | } 55 | 56 | class _FlutterFlowCheckboxGroupState extends State { 57 | late List checkboxValues; 58 | late void Function() _selectedValueListener; 59 | ValueListenable?> get changeSelectedValues => widget.controller; 60 | List get selectedValues => widget.controller.value ?? []; 61 | 62 | @override 63 | void initState() { 64 | super.initState(); 65 | checkboxValues = List.from(widget.controller.initialValue ?? []); 66 | if (!widget.initialized && checkboxValues.isNotEmpty) { 67 | SchedulerBinding.instance.addPostFrameCallback( 68 | (_) { 69 | if (widget.onChanged != null) { 70 | widget.onChanged!(checkboxValues); 71 | } 72 | }, 73 | ); 74 | } 75 | _selectedValueListener = () { 76 | if (!listEquals(checkboxValues, selectedValues)) { 77 | setState(() => checkboxValues = List.from(selectedValues)); 78 | } 79 | if (widget.onChanged != null) { 80 | widget.onChanged!(selectedValues); 81 | } 82 | }; 83 | changeSelectedValues.addListener(_selectedValueListener); 84 | } 85 | 86 | @override 87 | void dispose() { 88 | changeSelectedValues.removeListener(_selectedValueListener); 89 | super.dispose(); 90 | } 91 | 92 | @override 93 | Widget build(BuildContext context) => ListView.builder( 94 | physics: const NeverScrollableScrollPhysics(), 95 | shrinkWrap: true, 96 | padding: EdgeInsets.zero, 97 | itemCount: widget.options.length, 98 | itemBuilder: (context, index) { 99 | final option = widget.options[index]; 100 | final selected = selectedValues.contains(option); 101 | final unselectedTextStyle = 102 | widget.unselectedTextStyle ?? widget.textStyle; 103 | return Theme( 104 | data: ThemeData(unselectedWidgetColor: widget.checkboxBorderColor), 105 | child: Padding( 106 | padding: widget.itemPadding ?? EdgeInsets.zero, 107 | child: Row( 108 | children: [ 109 | Checkbox( 110 | value: selected, 111 | onChanged: widget.onChanged != null 112 | ? (isSelected) { 113 | if (isSelected == null) { 114 | return; 115 | } 116 | isSelected 117 | ? checkboxValues.add(option) 118 | : checkboxValues.remove(option); 119 | widget.controller.value = List.from(checkboxValues); 120 | setState(() {}); 121 | } 122 | : null, 123 | activeColor: widget.activeColor, 124 | checkColor: widget.checkColor, 125 | shape: RoundedRectangleBorder( 126 | borderRadius: 127 | widget.checkboxBorderRadius ?? BorderRadius.zero, 128 | ), 129 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 130 | visualDensity: VisualDensity.compact, 131 | ), 132 | Expanded( 133 | child: Padding( 134 | padding: widget.labelPadding ?? EdgeInsets.zero, 135 | child: Text( 136 | widget.options[index], 137 | style: 138 | selected ? widget.textStyle : unselectedTextStyle, 139 | ), 140 | ), 141 | ), 142 | ], 143 | ), 144 | ), 145 | ); 146 | }, 147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /lib/src/utils/flutter_flow_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/scheduler.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | Widget wrapWithModel({ 7 | required T model, 8 | required Widget child, 9 | required VoidCallback updateCallback, 10 | bool updateOnChange = false, 11 | }) { 12 | // Set the component to optionally update the page on updates. 13 | model 14 | ..setOnUpdate( 15 | onUpdate: updateCallback, 16 | updateOnChange: updateOnChange, 17 | ) 18 | // Models for components within a page will be disposed by the page's model, 19 | // so we don't want the component widget to dispose them until the page is 20 | // itself disposed. 21 | ..disposeOnWidgetDisposal = false; 22 | // Wrap in a Provider so that the model can be accessed by the component. 23 | return Provider.value( 24 | value: model, 25 | child: child, 26 | ); 27 | } 28 | 29 | T createModel( 30 | BuildContext context, 31 | T Function() defaultBuilder, 32 | ) { 33 | final model = context.read() ?? defaultBuilder() 34 | .._init(context); 35 | return model; 36 | } 37 | 38 | abstract class FlutterFlowModel { 39 | // Initialization methods 40 | bool _isInitialized = false; 41 | void initState(BuildContext context); 42 | void _init(BuildContext context) { 43 | if (!_isInitialized) { 44 | initState(context); 45 | _isInitialized = true; 46 | } 47 | if (context.widget is W) { 48 | _widget = context.widget as W; 49 | } 50 | } 51 | 52 | // The widget associated with this model. This is useful for accessing the 53 | // parameters of the widget, for example. 54 | W? _widget; 55 | // This will always be non-null when used, but is nullable to allow us to 56 | // dispose of the widget in the [dispose] method (for garbage collection). 57 | W get widget => _widget!; 58 | 59 | // Dispose methods 60 | // Whether to dispose this model when the corresponding widget is 61 | // disposed. By default this is true for pages and false for components, 62 | // as page/component models handle the disposal of their children. 63 | bool disposeOnWidgetDisposal = true; 64 | void dispose(); 65 | void maybeDispose() { 66 | if (disposeOnWidgetDisposal) { 67 | dispose(); 68 | } 69 | // Remove reference to widget for garbage collection purposes. 70 | _widget = null; 71 | } 72 | 73 | // Whether to update the containing page / component on updates. 74 | bool updateOnChange = false; 75 | // Function to call when the model receives an update. 76 | VoidCallback _updateCallback = () {}; 77 | void onUpdate() => updateOnChange ? _updateCallback() : () {}; 78 | FlutterFlowModel setOnUpdate({ 79 | bool updateOnChange = false, 80 | required VoidCallback onUpdate, 81 | }) => 82 | this 83 | .._updateCallback = onUpdate 84 | ..updateOnChange = updateOnChange; 85 | // Update the containing page when this model received an update. 86 | void updatePage(VoidCallback callback) { 87 | callback(); 88 | _updateCallback(); 89 | } 90 | } 91 | 92 | class FlutterFlowDynamicModels { 93 | FlutterFlowDynamicModels(this.defaultBuilder); 94 | 95 | final T Function() defaultBuilder; 96 | final Map _childrenModels = {}; 97 | final Map _childrenIndexes = {}; 98 | Set? _activeKeys; 99 | 100 | T getModel(String uniqueKey, int index) { 101 | _updateActiveKeys(uniqueKey); 102 | _childrenIndexes[uniqueKey] = index; 103 | return _childrenModels[uniqueKey] ??= defaultBuilder(); 104 | } 105 | 106 | List getValues(S? Function(T) getValue) { 107 | return _childrenIndexes.entries 108 | // Sort keys by index. 109 | .sorted((a, b) => a.value.compareTo(b.value)) 110 | .where((e) => _childrenModels[e.key] != null) 111 | // Map each model to the desired value and return as list. In order 112 | // to preserve index order, rather than removing null values we provide 113 | // default values (for types with reasonable defaults). 114 | .map((e) => getValue(_childrenModels[e.key]!) ?? _getDefaultValue()!) 115 | .toList(); 116 | } 117 | 118 | S? getValueAtIndex(int index, S? Function(T) getValue) { 119 | final uniqueKey = 120 | _childrenIndexes.entries.firstWhereOrNull((e) => e.value == index)?.key; 121 | return getValueForKey(uniqueKey, getValue); 122 | } 123 | 124 | S? getValueForKey(String? uniqueKey, S? Function(T) getValue) { 125 | final model = _childrenModels[uniqueKey]; 126 | return model != null ? getValue(model) : null; 127 | } 128 | 129 | void dispose() => _childrenModels.values.forEach((model) => model.dispose()); 130 | 131 | void _updateActiveKeys(String uniqueKey) { 132 | final shouldResetActiveKeys = _activeKeys == null; 133 | _activeKeys ??= {}; 134 | _activeKeys!.add(uniqueKey); 135 | 136 | if (shouldResetActiveKeys) { 137 | // Add a post-frame callback to remove and dispose of unused models after 138 | // we're done building, then reset `_activeKeys` to null so we know to do 139 | // this again next build. 140 | SchedulerBinding.instance.addPostFrameCallback((_) { 141 | _childrenIndexes.removeWhere((k, _) => !_activeKeys!.contains(k)); 142 | _childrenModels.keys 143 | .toSet() 144 | .difference(_activeKeys!) 145 | // Remove and dispose of unused models since they are not being used 146 | // elsewhere and would not otherwise be disposed. 147 | .forEach((k) => _childrenModels.remove(k)?.maybeDispose()); 148 | _activeKeys = null; 149 | }); 150 | } 151 | } 152 | } 153 | 154 | T? _getDefaultValue() { 155 | switch (T) { 156 | case const (int): 157 | return 0 as T; 158 | case const (double): 159 | return 0.0 as T; 160 | case const (String): 161 | return '' as T; 162 | case const (bool): 163 | return false as T; 164 | default: 165 | return null as T; 166 | } 167 | } 168 | 169 | extension TextValidationExtensions on String? Function(BuildContext, String?)? { 170 | String? Function(String?)? asValidator(BuildContext context) => 171 | this != null ? (val) => this!(context, val) : null; 172 | } 173 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 3 | 4 | /// A customizable icon button widget. 5 | class FlutterFlowIconButton extends StatefulWidget { 6 | /// Creates a [FlutterFlowIconButton]. 7 | /// 8 | /// - [icon] parameter is required and specifies the widget to be used as the icon. 9 | /// - [borderRadius] parameter specifies the border radius of the button. 10 | /// - [buttonSize] parameter specifies the size of the button. 11 | /// - [fillColor] parameter specifies the fill color of the button. 12 | /// - [disabledColor] parameter specifies the color of the button when it is disabled. 13 | /// - [disabledIconColor] parameter specifies the color of the icon when the button is disabled. 14 | /// - [hoverColor] parameter specifies the color of the button when it is hovered. 15 | /// - [hoverIconColor] parameter specifies the color of the icon when the button is hovered. 16 | /// - [borderColor] parameter specifies the border color of the button. 17 | /// - [borderWidth] parameter specifies the width of the button's border. 18 | /// - [showLoadingIndicator] parameter specifies whether to show a loading indicator on the button. 19 | /// - [onPressed] parameter specifies the callback function to be called when the button is pressed. 20 | const FlutterFlowIconButton({ 21 | super.key, 22 | required this.icon, 23 | this.borderColor, 24 | this.borderRadius, 25 | this.borderWidth, 26 | this.buttonSize, 27 | this.fillColor, 28 | this.disabledColor, 29 | this.disabledIconColor, 30 | this.hoverColor, 31 | this.hoverIconColor, 32 | this.onPressed, 33 | this.showLoadingIndicator = false, 34 | }); 35 | 36 | final Widget icon; 37 | final double? borderRadius; 38 | final double? buttonSize; 39 | final Color? fillColor; 40 | final Color? disabledColor; 41 | final Color? disabledIconColor; 42 | final Color? hoverColor; 43 | final Color? hoverIconColor; 44 | final Color? borderColor; 45 | final double? borderWidth; 46 | final bool showLoadingIndicator; 47 | final Function()? onPressed; 48 | 49 | @override 50 | State createState() => _FlutterFlowIconButtonState(); 51 | } 52 | 53 | class _FlutterFlowIconButtonState extends State { 54 | bool loading = false; 55 | late double? iconSize; 56 | late Color? iconColor; 57 | late Widget effectiveIcon; 58 | 59 | @override 60 | void initState() { 61 | super.initState(); 62 | _updateIcon(); 63 | } 64 | 65 | @override 66 | void didUpdateWidget(FlutterFlowIconButton oldWidget) { 67 | super.didUpdateWidget(oldWidget); 68 | _updateIcon(); 69 | } 70 | 71 | void _updateIcon() { 72 | final isFontAwesome = widget.icon is FaIcon; 73 | if (isFontAwesome) { 74 | FaIcon icon = widget.icon as FaIcon; 75 | effectiveIcon = FaIcon( 76 | icon.icon, 77 | size: icon.size, 78 | ); 79 | iconSize = icon.size; 80 | iconColor = icon.color; 81 | } else { 82 | Icon icon = widget.icon as Icon; 83 | effectiveIcon = Icon( 84 | icon.icon, 85 | size: icon.size, 86 | ); 87 | iconSize = icon.size; 88 | iconColor = icon.color; 89 | } 90 | } 91 | 92 | @override 93 | Widget build(BuildContext context) { 94 | ButtonStyle style = ButtonStyle( 95 | shape: WidgetStateProperty.resolveWith( 96 | (states) { 97 | return RoundedRectangleBorder( 98 | borderRadius: BorderRadius.circular(widget.borderRadius ?? 0), 99 | side: BorderSide( 100 | color: widget.borderColor ?? Colors.transparent, 101 | width: widget.borderWidth ?? 0, 102 | ), 103 | ); 104 | }, 105 | ), 106 | iconColor: WidgetStateProperty.resolveWith( 107 | (states) { 108 | if (states.contains(WidgetState.disabled) && 109 | widget.disabledIconColor != null) { 110 | return widget.disabledIconColor; 111 | } 112 | if (states.contains(WidgetState.hovered) && 113 | widget.hoverIconColor != null) { 114 | return widget.hoverIconColor; 115 | } 116 | return iconColor; 117 | }, 118 | ), 119 | backgroundColor: WidgetStateProperty.resolveWith( 120 | (states) { 121 | if (states.contains(WidgetState.disabled) && 122 | widget.disabledColor != null) { 123 | return widget.disabledColor; 124 | } 125 | if (states.contains(WidgetState.hovered) && 126 | widget.hoverColor != null) { 127 | return widget.hoverColor; 128 | } 129 | 130 | return widget.fillColor; 131 | }, 132 | ), 133 | overlayColor: WidgetStateProperty.resolveWith((states) { 134 | if (states.contains(WidgetState.pressed)) { 135 | return null; 136 | } 137 | return widget.hoverColor == null ? null : Colors.transparent; 138 | }), 139 | ); 140 | 141 | return SizedBox( 142 | width: widget.buttonSize, 143 | height: widget.buttonSize, 144 | child: Theme( 145 | data: ThemeData.from( 146 | colorScheme: Theme.of(context).colorScheme, 147 | useMaterial3: true, 148 | ), 149 | child: IgnorePointer( 150 | ignoring: widget.showLoadingIndicator && loading, 151 | child: IconButton( 152 | icon: (widget.showLoadingIndicator && loading) 153 | ? SizedBox( 154 | width: iconSize, 155 | height: iconSize, 156 | child: CircularProgressIndicator( 157 | valueColor: AlwaysStoppedAnimation( 158 | iconColor ?? Colors.white, 159 | ), 160 | ), 161 | ) 162 | : effectiveIcon, 163 | onPressed: widget.onPressed == null 164 | ? null 165 | : () async { 166 | if (loading) { 167 | return; 168 | } 169 | setState(() => loading = true); 170 | try { 171 | await widget.onPressed!(); 172 | } finally { 173 | if (mounted) { 174 | setState(() => loading = false); 175 | } 176 | } 177 | }, 178 | splashRadius: widget.buttonSize, 179 | style: style, 180 | ), 181 | ), 182 | ), 183 | ); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lib/src/utils/flutter_flow_helpers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart' show kIsWeb; 4 | import 'package:flutter/material.dart'; 5 | import 'package:from_css_color/from_css_color.dart'; 6 | import 'package:intl/intl.dart'; 7 | import 'package:json_path/json_path.dart'; 8 | import 'package:timeago/timeago.dart' as timeago; 9 | 10 | export 'dart:convert' show jsonEncode, jsonDecode; 11 | export 'dart:math' show min, max; 12 | export 'dart:typed_data' show Uint8List; 13 | 14 | export 'package:intl/intl.dart'; 15 | export 'package:page_transition/page_transition.dart'; 16 | 17 | export 'flutter_flow_model.dart'; 18 | export 'lat_lng.dart'; 19 | export 'place.dart'; 20 | 21 | final RouteObserver routeObserver = RouteObserver(); 22 | 23 | T valueOrDefault(T? value, T defaultValue) => 24 | (value is String && value.isEmpty) || value == null ? defaultValue : value; 25 | 26 | String dateTimeFormat(String format, DateTime? dateTime, {String? locale}) { 27 | if (dateTime == null) { 28 | return ''; 29 | } 30 | if (format == 'relative') { 31 | return timeago.format(dateTime, locale: locale, allowFromNow: true); 32 | } 33 | return DateFormat(format, locale).format(dateTime); 34 | } 35 | 36 | Color colorFromCssString(String color, {Color? defaultColor}) { 37 | try { 38 | return fromCssColor(color); 39 | } catch (_) {} 40 | return defaultColor ?? Colors.black; 41 | } 42 | 43 | enum FormatType { 44 | decimal, 45 | percent, 46 | scientific, 47 | compact, 48 | compactLong, 49 | custom, 50 | } 51 | 52 | enum DecimalType { 53 | automatic, 54 | periodDecimal, 55 | commaDecimal, 56 | } 57 | 58 | String formatNumber( 59 | num? value, { 60 | required FormatType formatType, 61 | DecimalType? decimalType, 62 | String? currency, 63 | bool toLowerCase = false, 64 | String? format, 65 | String? locale, 66 | }) { 67 | if (value == null) { 68 | return ''; 69 | } 70 | var formattedValue = ''; 71 | switch (formatType) { 72 | case FormatType.decimal: 73 | switch (decimalType!) { 74 | case DecimalType.automatic: 75 | formattedValue = NumberFormat.decimalPattern().format(value); 76 | break; 77 | case DecimalType.periodDecimal: 78 | formattedValue = NumberFormat.decimalPattern('en_US').format(value); 79 | break; 80 | case DecimalType.commaDecimal: 81 | formattedValue = NumberFormat.decimalPattern('es_PA').format(value); 82 | break; 83 | } 84 | break; 85 | case FormatType.percent: 86 | formattedValue = NumberFormat.percentPattern().format(value); 87 | break; 88 | case FormatType.scientific: 89 | formattedValue = NumberFormat.scientificPattern().format(value); 90 | if (toLowerCase) { 91 | formattedValue = formattedValue.toLowerCase(); 92 | } 93 | break; 94 | case FormatType.compact: 95 | formattedValue = NumberFormat.compact().format(value); 96 | break; 97 | case FormatType.compactLong: 98 | formattedValue = NumberFormat.compactLong().format(value); 99 | break; 100 | case FormatType.custom: 101 | final hasLocale = locale != null && locale.isNotEmpty; 102 | formattedValue = 103 | NumberFormat(format, hasLocale ? locale : null).format(value); 104 | } 105 | 106 | if (formattedValue.isEmpty) { 107 | return value.toString(); 108 | } 109 | 110 | if (currency != null) { 111 | final currencySymbol = currency.isNotEmpty 112 | ? currency 113 | : NumberFormat.simpleCurrency().format(0.0).substring(0, 1); 114 | formattedValue = '$currencySymbol$formattedValue'; 115 | } 116 | 117 | return formattedValue; 118 | } 119 | 120 | DateTime get getCurrentTimestamp => DateTime.now(); 121 | DateTime dateTimeFromSecondsSinceEpoch(int seconds) { 122 | return DateTime.fromMillisecondsSinceEpoch(seconds * 1000); 123 | } 124 | 125 | extension DateTimeConversionExtension on DateTime { 126 | int get secondsSinceEpoch => (millisecondsSinceEpoch / 1000).round(); 127 | } 128 | 129 | extension DateTimeComparisonOperators on DateTime { 130 | bool operator <(DateTime other) => isBefore(other); 131 | bool operator >(DateTime other) => isAfter(other); 132 | bool operator <=(DateTime other) => this < other || isAtSameMomentAs(other); 133 | bool operator >=(DateTime other) => this > other || isAtSameMomentAs(other); 134 | } 135 | 136 | dynamic getJsonField( 137 | dynamic response, 138 | String jsonPath, [ 139 | bool isForList = false, 140 | ]) { 141 | final field = JsonPath(jsonPath).read(response); 142 | if (field.isEmpty) { 143 | return null; 144 | } 145 | if (field.length > 1) { 146 | return field.map((f) => f.value).toList(); 147 | } 148 | final value = field.first.value; 149 | return isForList && value is! Iterable ? [value] : value; 150 | } 151 | 152 | Rect? getWidgetBoundingBox(BuildContext context) { 153 | try { 154 | final renderBox = context.findRenderObject() as RenderBox?; 155 | return renderBox!.localToGlobal(Offset.zero) & renderBox.size; 156 | } catch (_) { 157 | return null; 158 | } 159 | } 160 | 161 | bool get isAndroid => !kIsWeb && Platform.isAndroid; 162 | bool get isiOS => !kIsWeb && Platform.isIOS; 163 | bool get isWeb => kIsWeb; 164 | 165 | const kBreakpointSmall = 479.0; 166 | const kBreakpointMedium = 767.0; 167 | const kBreakpointLarge = 991.0; 168 | bool isMobileWidth(BuildContext context) => 169 | MediaQuery.sizeOf(context).width < kBreakpointSmall; 170 | bool responsiveVisibility({ 171 | required BuildContext context, 172 | bool phone = true, 173 | bool tablet = true, 174 | bool tabletLandscape = true, 175 | bool desktop = true, 176 | }) { 177 | final width = MediaQuery.sizeOf(context).width; 178 | if (width < kBreakpointSmall) { 179 | return phone; 180 | } else if (width < kBreakpointMedium) { 181 | return tablet; 182 | } else if (width < kBreakpointLarge) { 183 | return tabletLandscape; 184 | } else { 185 | return desktop; 186 | } 187 | } 188 | 189 | const kTextValidatorUsernameRegex = r'^[a-zA-Z][a-zA-Z0-9_-]{2,16}$'; 190 | // https://stackoverflow.com/a/201378 191 | const kTextValidatorEmailRegex = 192 | "^(?:[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])+)\\])\$"; 193 | const kTextValidatorWebsiteRegex = 194 | 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@:%_\+.~#?&//=]*)'; 195 | 196 | extension FFTextEditingControllerExt on TextEditingController? { 197 | String get text => this == null ? '' : this!.text; 198 | set text(String newText) => this?.text = newText; 199 | } 200 | 201 | extension IterableExt on Iterable { 202 | List mapIndexed(S Function(int, T) func) => toList() 203 | .asMap() 204 | .map((index, value) => MapEntry(index, func(index, value))) 205 | .values 206 | .toList(); 207 | } 208 | 209 | void showSnackbar( 210 | BuildContext context, 211 | String message, { 212 | bool loading = false, 213 | int duration = 4, 214 | }) { 215 | ScaffoldMessenger.of(context).hideCurrentSnackBar(); 216 | ScaffoldMessenger.of(context).showSnackBar( 217 | SnackBar( 218 | content: Row( 219 | children: [ 220 | if (loading) 221 | const Padding( 222 | padding: EdgeInsetsDirectional.only(end: 10.0), 223 | child: SizedBox( 224 | height: 20, 225 | width: 20, 226 | child: CircularProgressIndicator( 227 | color: Colors.white, 228 | ), 229 | ), 230 | ), 231 | Text(message), 232 | ], 233 | ), 234 | duration: Duration(seconds: duration), 235 | ), 236 | ); 237 | } 238 | 239 | extension FFStringExt on String { 240 | String maybeHandleOverflow({int? maxChars, String replacement = ''}) => 241 | maxChars != null && length > maxChars 242 | ? replaceRange(maxChars, null, replacement) 243 | : this; 244 | } 245 | 246 | extension ListFilterExt on Iterable { 247 | List get withoutNulls => where((s) => s != null).map((e) => e!).toList(); 248 | } 249 | 250 | extension ListDivideExt on Iterable { 251 | Iterable> get enumerate => toList().asMap().entries; 252 | 253 | List divide(Widget t) => isEmpty 254 | ? [] 255 | : (enumerate.map((e) => [e.value, t]).expand((i) => i).toList() 256 | ..removeLast()); 257 | 258 | List around(Widget t) => addToStart(t).addToEnd(t); 259 | 260 | List addToStart(Widget t) => 261 | enumerate.map((e) => e.value).toList()..insert(0, t); 262 | 263 | List addToEnd(Widget t) => 264 | enumerate.map((e) => e.value).toList()..add(t); 265 | 266 | List paddingTopEach(double val) => 267 | map((w) => Padding(padding: EdgeInsets.only(top: val), child: w)) 268 | .toList(); 269 | } 270 | 271 | extension StatefulWidgetExtensions on State { 272 | /// Check if the widget exist before safely setting state. 273 | void safeSetState(VoidCallback fn) { 274 | if (mounted) { 275 | // ignore: invalid_use_of_protected_member 276 | setState(fn); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_mux_broadcast.dart: -------------------------------------------------------------------------------- 1 | import 'package:apivideo_live_stream/apivideo_live_stream.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'flutter_flow_widgets.dart'; 4 | 5 | /// A widget that helps to create a live stream using the Mux API. 6 | class FlutterFlowMuxBroadcast extends StatefulWidget { 7 | const FlutterFlowMuxBroadcast({ 8 | super.key, 9 | required this.isCameraInitialized, 10 | required this.isStreaming, 11 | required this.durationString, 12 | this.borderRadius = BorderRadius.zero, 13 | required this.controller, 14 | required this.videoConfig, 15 | required this.onCameraRotateButtonTap, 16 | required this.startButtonText, 17 | required this.onStartButtonTap, 18 | required this.onStopButtonTap, 19 | required this.startButtonOptions, 20 | required this.startButtonIcon, 21 | required this.liveText, 22 | required this.liveTextStyle, 23 | required this.liveIcon, 24 | required this.liveTextBackgroundColor, 25 | this.liveContainerBorderRadius = BorderRadius.zero, 26 | required this.durationTextStyle, 27 | required this.durationTextBackgroundColor, 28 | this.durationContainerBorderRadius = BorderRadius.zero, 29 | required this.rotateButtonIcon, 30 | required this.rotateButtonColor, 31 | required this.stopButtonColor, 32 | required this.stopButtonIcon, 33 | }); 34 | 35 | /// Whether the camera is initialized or not. 36 | final bool isCameraInitialized; 37 | 38 | /// Whether the video is currently being streamed or not. 39 | final bool isStreaming; 40 | 41 | /// The duration of the video stream. 42 | final String? durationString; 43 | 44 | /// The border radius of the widget. 45 | final BorderRadius borderRadius; 46 | 47 | /// The controller for the live stream. 48 | final LiveStreamController? controller; 49 | 50 | /// The configuration for the video stream. 51 | final VideoConfig videoConfig; 52 | 53 | /// Callback function when the camera rotate button is tapped. 54 | final Function onCameraRotateButtonTap; 55 | 56 | /// The text for the start button. 57 | final String startButtonText; 58 | 59 | /// Callback function when the start button is tapped. 60 | final Function onStartButtonTap; 61 | 62 | /// Callback function when the stop button is tapped. 63 | final Function onStopButtonTap; 64 | 65 | /// The options for the start button. 66 | final FFButtonOptions startButtonOptions; 67 | 68 | /// The icon for the start button. 69 | final Widget startButtonIcon; 70 | 71 | /// The text for the live indicator. 72 | final String liveText; 73 | 74 | /// The style for the live indicator text. 75 | final TextStyle liveTextStyle; 76 | 77 | /// The icon for the live indicator. 78 | final Widget liveIcon; 79 | 80 | /// The background color for the live indicator. 81 | final Color liveTextBackgroundColor; 82 | 83 | /// The border radius for the live indicator container. 84 | final BorderRadius liveContainerBorderRadius; 85 | 86 | /// The style for the duration text. 87 | final TextStyle durationTextStyle; 88 | 89 | /// The background color for the duration text. 90 | final Color durationTextBackgroundColor; 91 | 92 | /// The border radius for the duration text container. 93 | final BorderRadius durationContainerBorderRadius; 94 | 95 | /// The icon for the rotate button. 96 | final Widget rotateButtonIcon; 97 | 98 | /// The color for the rotate button. 99 | final Color rotateButtonColor; 100 | 101 | /// The color for the stop button. 102 | final Color stopButtonColor; 103 | 104 | /// The icon for the stop button. 105 | final Widget stopButtonIcon; 106 | 107 | @override 108 | State createState() => 109 | _FlutterFlowMuxBroadcastState(); 110 | } 111 | 112 | class _FlutterFlowMuxBroadcastState extends State 113 | with WidgetsBindingObserver { 114 | @override 115 | void initState() { 116 | super.initState(); 117 | WidgetsBinding.instance.addObserver(this); 118 | } 119 | 120 | @override 121 | void dispose() { 122 | WidgetsBinding.instance.removeObserver(this); 123 | widget.controller?.stop(); 124 | super.dispose(); 125 | } 126 | 127 | @override 128 | void didChangeAppLifecycleState(AppLifecycleState state) { 129 | super.didChangeAppLifecycleState(state); 130 | if (state == AppLifecycleState.inactive) { 131 | final isStreaming = widget.controller?.isStreaming ?? false; 132 | if (isStreaming) { 133 | widget.onStopButtonTap(); 134 | } 135 | widget.controller?.stop(); 136 | } else if (state == AppLifecycleState.resumed) { 137 | widget.controller?.startPreview(); 138 | } 139 | } 140 | 141 | @override 142 | Widget build(BuildContext context) { 143 | return widget.isCameraInitialized 144 | ? ClipRRect( 145 | borderRadius: widget.borderRadius, 146 | child: CameraPreview( 147 | controller: widget.controller!, 148 | child: Padding( 149 | padding: const EdgeInsets.only( 150 | left: 16.0, 151 | right: 16.0, 152 | top: 16.0, 153 | bottom: 16.0, 154 | ), 155 | child: Stack( 156 | children: [ 157 | Align( 158 | alignment: Alignment.bottomLeft, 159 | child: widget.isStreaming 160 | ? Row( 161 | mainAxisAlignment: MainAxisAlignment.center, 162 | children: [ 163 | ClipRRect( 164 | borderRadius: BorderRadius.circular(50), 165 | child: InkWell( 166 | onTap: () => widget.onStopButtonTap(), 167 | child: Container( 168 | decoration: BoxDecoration( 169 | shape: BoxShape.circle, 170 | color: widget.stopButtonColor, 171 | ), 172 | child: Padding( 173 | padding: const EdgeInsets.all(16.0), 174 | child: widget.stopButtonIcon, 175 | ), 176 | ), 177 | ), 178 | ) 179 | ], 180 | ) 181 | : Row( 182 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 183 | children: [ 184 | InkWell( 185 | onTap: () => widget.onCameraRotateButtonTap(), 186 | child: CircleAvatar( 187 | radius: 188 | (widget.rotateButtonIcon as Icon).size, 189 | backgroundColor: widget.rotateButtonColor, 190 | child: Center( 191 | child: widget.rotateButtonIcon, 192 | ), 193 | ), 194 | ), 195 | FFButtonWidget( 196 | onPressed: () => widget.onStartButtonTap(), 197 | text: widget.startButtonText, 198 | icon: widget.startButtonIcon, 199 | options: widget.startButtonOptions, 200 | ) 201 | ], 202 | ), 203 | ), 204 | widget.isStreaming 205 | ? Row( 206 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 207 | children: [ 208 | Container( 209 | decoration: BoxDecoration( 210 | color: widget.liveTextBackgroundColor, 211 | borderRadius: 212 | widget.liveContainerBorderRadius, 213 | ), 214 | child: Padding( 215 | padding: const EdgeInsets.symmetric( 216 | horizontal: 16, 217 | vertical: 8, 218 | ), 219 | child: Row( 220 | mainAxisSize: MainAxisSize.min, 221 | children: [ 222 | widget.liveIcon, 223 | const SizedBox(width: 8), 224 | Text( 225 | widget.liveText, 226 | style: widget.liveTextStyle, 227 | ), 228 | ], 229 | ), 230 | ), 231 | ), 232 | Container( 233 | decoration: BoxDecoration( 234 | color: widget.durationTextBackgroundColor, 235 | borderRadius: 236 | widget.durationContainerBorderRadius, 237 | ), 238 | child: Padding( 239 | padding: const EdgeInsets.symmetric( 240 | horizontal: 16, 241 | vertical: 8, 242 | ), 243 | child: Text( 244 | widget.durationString ?? '00:00:00', 245 | style: widget.durationTextStyle, 246 | ), 247 | ), 248 | ), 249 | ], 250 | ) 251 | : const SizedBox(), 252 | ], 253 | ), 254 | ), 255 | ), 256 | ) 257 | : const Center( 258 | child: CircularProgressIndicator(), 259 | ); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_radio_button.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 https://github.com/TercyoStorck 3 | * 4 | * Source code has been modified by FlutterFlow, Inc. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 18 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 19 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 20 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 23 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 25 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | import 'package:flutter/material.dart'; 29 | import 'package:flutterflow_ui/src/utils/form_field_controller.dart'; 30 | 31 | /// A custom radio button widget that allows the user to select a single option from a list of options. 32 | class FlutterFlowRadioButton extends StatefulWidget { 33 | /// Creates a [FlutterFlowRadioButton]. 34 | const FlutterFlowRadioButton({ 35 | super.key, 36 | required this.options, 37 | required this.onChanged, 38 | required this.controller, 39 | required this.optionHeight, 40 | required this.textStyle, 41 | this.optionWidth, 42 | this.selectedTextStyle, 43 | this.textPadding = EdgeInsets.zero, 44 | this.buttonPosition = RadioButtonPosition.left, 45 | this.direction = Axis.vertical, 46 | required this.radioButtonColor, 47 | this.inactiveRadioButtonColor, 48 | this.toggleable = false, 49 | this.horizontalAlignment = WrapAlignment.start, 50 | this.verticalAlignment = WrapCrossAlignment.start, 51 | }); 52 | 53 | /// The list of options to choose from. 54 | final List options; 55 | 56 | /// A callback function that will be called when the selected option changes. 57 | final Function(String?)? onChanged; 58 | 59 | /// A form field controller that manages the state of the selected option. 60 | final FormFieldController controller; 61 | 62 | /// The height of each option. 63 | final double optionHeight; 64 | 65 | /// The width of each option. If not provided, the width will be determined automatically. 66 | final double? optionWidth; 67 | 68 | /// The style of the option text. 69 | final TextStyle textStyle; 70 | 71 | /// The style of the selected option text. If not provided, the [textStyle] will be used. 72 | final TextStyle? selectedTextStyle; 73 | 74 | /// The padding around the option text. 75 | final EdgeInsetsGeometry textPadding; 76 | 77 | /// The position of the radio button relative to the option text. 78 | final RadioButtonPosition buttonPosition; 79 | 80 | /// The direction in which the options are laid out. 81 | final Axis direction; 82 | 83 | /// The color of the radio button. 84 | final Color radioButtonColor; 85 | 86 | /// The color of the radio button when it is not selected. If not provided, the [radioButtonColor] will be used. 87 | final Color? inactiveRadioButtonColor; 88 | 89 | /// Whether the radio button can be toggled on and off. 90 | final bool toggleable; 91 | 92 | /// The horizontal alignment of the options when the direction is horizontal. 93 | final WrapAlignment horizontalAlignment; 94 | 95 | /// The vertical alignment of the options when the direction is vertical. 96 | final WrapCrossAlignment verticalAlignment; 97 | 98 | @override 99 | State createState() => _FlutterFlowRadioButtonState(); 100 | } 101 | 102 | class _FlutterFlowRadioButtonState extends State { 103 | bool get enabled => widget.onChanged != null; 104 | FormFieldController get controller => widget.controller; 105 | void Function()? _listener; 106 | 107 | @override 108 | void initState() { 109 | super.initState(); 110 | _maybeSetOnChangedListener(); 111 | } 112 | 113 | @override 114 | void dispose() { 115 | _maybeRemoveOnChangedListener(); 116 | super.dispose(); 117 | } 118 | 119 | @override 120 | void didUpdateWidget(FlutterFlowRadioButton oldWidget) { 121 | super.didUpdateWidget(oldWidget); 122 | final oldWidgetEnabled = oldWidget.onChanged != null; 123 | if (oldWidgetEnabled != enabled) { 124 | _maybeRemoveOnChangedListener(); 125 | _maybeSetOnChangedListener(); 126 | } 127 | } 128 | 129 | void _maybeSetOnChangedListener() { 130 | if (enabled) { 131 | _listener = () => widget.onChanged!(controller.value); 132 | controller.addListener(_listener!); 133 | } 134 | } 135 | 136 | void _maybeRemoveOnChangedListener() { 137 | if (_listener != null) { 138 | controller.removeListener(_listener!); 139 | _listener = null; 140 | } 141 | } 142 | 143 | List get effectiveOptions => 144 | widget.options.isEmpty ? ['[Option]'] : widget.options; 145 | 146 | @override 147 | Widget build(BuildContext context) { 148 | return Theme( 149 | data: Theme.of(context) 150 | .copyWith(unselectedWidgetColor: widget.inactiveRadioButtonColor), 151 | child: RadioGroup.builder( 152 | direction: widget.direction, 153 | groupValue: controller.value, 154 | onChanged: enabled ? (value) => controller.value = value : null, 155 | activeColor: widget.radioButtonColor, 156 | toggleable: widget.toggleable, 157 | textStyle: widget.textStyle, 158 | selectedTextStyle: widget.selectedTextStyle ?? widget.textStyle, 159 | textPadding: widget.textPadding, 160 | optionHeight: widget.optionHeight, 161 | optionWidth: widget.optionWidth, 162 | horizontalAlignment: widget.horizontalAlignment, 163 | verticalAlignment: widget.verticalAlignment, 164 | items: effectiveOptions, 165 | itemBuilder: (item) => 166 | RadioButtonBuilder(item, buttonPosition: widget.buttonPosition), 167 | ), 168 | ); 169 | } 170 | } 171 | 172 | enum RadioButtonPosition { 173 | right, 174 | left, 175 | } 176 | 177 | class RadioButtonBuilder { 178 | RadioButtonBuilder( 179 | this.description, { 180 | this.buttonPosition = RadioButtonPosition.left, 181 | }); 182 | 183 | final String description; 184 | final RadioButtonPosition buttonPosition; 185 | } 186 | 187 | class RadioButton extends StatelessWidget { 188 | const RadioButton({ 189 | super.key, 190 | required this.description, 191 | required this.value, 192 | required this.groupValue, 193 | required this.onChanged, 194 | required this.buttonPosition, 195 | required this.activeColor, 196 | required this.toggleable, 197 | required this.textStyle, 198 | required this.selectedTextStyle, 199 | required this.textPadding, 200 | this.shouldFlex = false, 201 | }); 202 | 203 | final String description; 204 | final T value; 205 | final T? groupValue; 206 | final void Function(T?)? onChanged; 207 | final RadioButtonPosition buttonPosition; 208 | final Color activeColor; 209 | final bool toggleable; 210 | final TextStyle textStyle; 211 | final TextStyle selectedTextStyle; 212 | final EdgeInsetsGeometry textPadding; 213 | final bool shouldFlex; 214 | 215 | @override 216 | Widget build(BuildContext context) { 217 | final selectedStyle = selectedTextStyle; 218 | final isSelected = value == groupValue; 219 | Widget radioButtonText = Padding( 220 | padding: textPadding, 221 | child: Text( 222 | description, 223 | style: isSelected ? selectedStyle : textStyle, 224 | ), 225 | ); 226 | if (shouldFlex) { 227 | radioButtonText = Flexible(child: radioButtonText); 228 | } 229 | return InkWell( 230 | onTap: onChanged != null ? () => onChanged!(value) : null, 231 | child: Row( 232 | mainAxisSize: MainAxisSize.min, 233 | children: [ 234 | if (buttonPosition == RadioButtonPosition.right) radioButtonText, 235 | Radio( 236 | groupValue: groupValue, 237 | onChanged: onChanged, 238 | value: value, 239 | activeColor: activeColor, 240 | toggleable: toggleable, 241 | ), 242 | if (buttonPosition == RadioButtonPosition.left) radioButtonText, 243 | ], 244 | ), 245 | ); 246 | } 247 | } 248 | 249 | class RadioGroup extends StatelessWidget { 250 | const RadioGroup.builder({ 251 | super.key, 252 | required this.groupValue, 253 | required this.onChanged, 254 | required this.items, 255 | required this.itemBuilder, 256 | required this.direction, 257 | required this.optionHeight, 258 | required this.horizontalAlignment, 259 | required this.activeColor, 260 | required this.toggleable, 261 | required this.textStyle, 262 | required this.selectedTextStyle, 263 | required this.textPadding, 264 | this.optionWidth, 265 | this.verticalAlignment = WrapCrossAlignment.center, 266 | }); 267 | 268 | final T? groupValue; 269 | final List items; 270 | final RadioButtonBuilder Function(T value) itemBuilder; 271 | final void Function(T?)? onChanged; 272 | final Axis direction; 273 | final double optionHeight; 274 | final double? optionWidth; 275 | final WrapAlignment horizontalAlignment; 276 | final WrapCrossAlignment verticalAlignment; 277 | final Color activeColor; 278 | final bool toggleable; 279 | final TextStyle textStyle; 280 | final TextStyle selectedTextStyle; 281 | final EdgeInsetsGeometry textPadding; 282 | 283 | List get _group => items.map( 284 | (item) { 285 | final radioButtonBuilder = itemBuilder(item); 286 | return SizedBox( 287 | height: optionHeight, 288 | width: optionWidth, 289 | child: RadioButton( 290 | description: radioButtonBuilder.description, 291 | value: item, 292 | groupValue: groupValue, 293 | onChanged: onChanged, 294 | buttonPosition: radioButtonBuilder.buttonPosition, 295 | activeColor: activeColor, 296 | toggleable: toggleable, 297 | textStyle: textStyle, 298 | selectedTextStyle: selectedTextStyle, 299 | textPadding: textPadding, 300 | shouldFlex: optionWidth != null, 301 | ), 302 | ); 303 | }, 304 | ).toList(); 305 | 306 | @override 307 | Widget build(BuildContext context) => direction == Axis.horizontal 308 | ? Wrap( 309 | direction: direction, 310 | alignment: horizontalAlignment, 311 | children: _group, 312 | ) 313 | : Wrap( 314 | direction: direction, 315 | crossAxisAlignment: verticalAlignment, 316 | children: _group, 317 | ); 318 | } 319 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_size_text/auto_size_text.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 4 | 5 | class FFButtonOptions { 6 | const FFButtonOptions({ 7 | this.textAlign, 8 | this.textStyle, 9 | this.elevation, 10 | this.height, 11 | this.width, 12 | this.padding, 13 | this.color, 14 | this.disabledColor, 15 | this.disabledTextColor, 16 | this.splashColor, 17 | this.iconSize, 18 | this.iconColor, 19 | this.iconPadding, 20 | this.borderRadius, 21 | this.borderSide, 22 | this.hoverColor, 23 | this.hoverBorderSide, 24 | this.hoverTextColor, 25 | this.hoverElevation, 26 | this.maxLines, 27 | }); 28 | 29 | /// The alignment of the button's text within its bounds. 30 | final TextAlign? textAlign; 31 | 32 | /// The style of the button's text. 33 | final TextStyle? textStyle; 34 | 35 | /// The elevation of the button. 36 | final double? elevation; 37 | 38 | /// The height of the button. 39 | final double? height; 40 | 41 | /// The width of the button. 42 | final double? width; 43 | 44 | /// The padding around the button's content. 45 | final EdgeInsetsGeometry? padding; 46 | 47 | /// The background color of the button. 48 | final Color? color; 49 | 50 | /// The background color of the button when it is disabled. 51 | final Color? disabledColor; 52 | 53 | /// The text color of the button when it is disabled. 54 | final Color? disabledTextColor; 55 | 56 | /// The maximum number of lines for the button's text. 57 | final int? maxLines; 58 | 59 | /// The color of the splash effect when the button is pressed. 60 | final Color? splashColor; 61 | 62 | /// The size of the button's icon. 63 | final double? iconSize; 64 | 65 | /// The color of the button's icon. 66 | final Color? iconColor; 67 | 68 | /// The padding around the button's icon. 69 | final EdgeInsetsGeometry? iconPadding; 70 | 71 | /// The border radius of the button. 72 | final BorderRadius? borderRadius; 73 | 74 | /// The border of the button. 75 | final BorderSide? borderSide; 76 | 77 | /// The background color of the button when it is hovered. 78 | final Color? hoverColor; 79 | 80 | /// The border of the button when it is hovered. 81 | final BorderSide? hoverBorderSide; 82 | 83 | /// The text color of the button when it is hovered. 84 | final Color? hoverTextColor; 85 | 86 | /// The elevation of the button when it is hovered. 87 | final double? hoverElevation; 88 | } 89 | 90 | /// A customizable button widget that can display text, an icon, and a loading indicator. 91 | class FFButtonWidget extends StatefulWidget { 92 | /// Creates a [FFButtonWidget]. 93 | /// 94 | /// - [text] parameter is required and specifies the text to be displayed on the button. 95 | /// - [onPressed] parameter is a callback function that will be called when the button is pressed. 96 | /// - [icon] parameter is an optional widget that can be used to display an icon alongside the text. 97 | /// - [iconData] parameter is an optional icon data that can be used to display an icon alongside the text. 98 | /// - [options] parameter is required and specifies the visual options for the button. 99 | /// - [showLoadingIndicator] parameter is an optional boolean value that determines whether to show a loading indicator when the button is pressed. 100 | const FFButtonWidget({ 101 | super.key, 102 | required this.text, 103 | required this.onPressed, 104 | this.icon, 105 | this.iconData, 106 | required this.options, 107 | this.showLoadingIndicator = true, 108 | }); 109 | 110 | final String text; 111 | final Widget? icon; 112 | final IconData? iconData; 113 | final Function()? onPressed; 114 | final FFButtonOptions options; 115 | final bool showLoadingIndicator; 116 | 117 | @override 118 | State createState() => _FFButtonWidgetState(); 119 | } 120 | 121 | class _FFButtonWidgetState extends State { 122 | bool loading = false; 123 | 124 | int get maxLines => widget.options.maxLines ?? 1; 125 | 126 | String? get text => 127 | widget.options.textStyle?.fontSize == 0 ? null : widget.text; 128 | 129 | @override 130 | Widget build(BuildContext context) { 131 | Widget textWidget = loading 132 | ? SizedBox( 133 | width: widget.options.width == null 134 | ? _getTextWidth(text, widget.options.textStyle, maxLines) 135 | : null, 136 | child: Center( 137 | child: SizedBox( 138 | width: 23, 139 | height: 23, 140 | child: CircularProgressIndicator( 141 | valueColor: AlwaysStoppedAnimation( 142 | widget.options.textStyle?.color ?? Colors.white, 143 | ), 144 | ), 145 | ), 146 | ), 147 | ) 148 | : AutoSizeText( 149 | text ?? '', 150 | style: 151 | text == null ? null : widget.options.textStyle?.withoutColor(), 152 | textAlign: widget.options.textAlign, 153 | maxLines: maxLines, 154 | overflow: TextOverflow.ellipsis, 155 | ); 156 | 157 | final onPressed = widget.onPressed != null 158 | ? (widget.showLoadingIndicator 159 | ? () async { 160 | if (loading) { 161 | return; 162 | } 163 | setState(() => loading = true); 164 | try { 165 | await widget.onPressed!(); 166 | } finally { 167 | if (mounted) { 168 | setState(() => loading = false); 169 | } 170 | } 171 | } 172 | : () => widget.onPressed!()) 173 | : null; 174 | 175 | ButtonStyle style = ButtonStyle( 176 | shape: WidgetStateProperty.resolveWith( 177 | (states) { 178 | if (states.contains(WidgetState.hovered) && 179 | widget.options.hoverBorderSide != null) { 180 | return RoundedRectangleBorder( 181 | borderRadius: 182 | widget.options.borderRadius ?? BorderRadius.circular(8), 183 | side: widget.options.hoverBorderSide!, 184 | ); 185 | } 186 | return RoundedRectangleBorder( 187 | borderRadius: 188 | widget.options.borderRadius ?? BorderRadius.circular(8), 189 | side: widget.options.borderSide ?? BorderSide.none, 190 | ); 191 | }, 192 | ), 193 | foregroundColor: WidgetStateProperty.resolveWith( 194 | (states) { 195 | if (states.contains(WidgetState.disabled) && 196 | widget.options.disabledTextColor != null) { 197 | return widget.options.disabledTextColor; 198 | } 199 | if (states.contains(WidgetState.hovered) && 200 | widget.options.hoverTextColor != null) { 201 | return widget.options.hoverTextColor; 202 | } 203 | return widget.options.textStyle?.color ?? Colors.white; 204 | }, 205 | ), 206 | backgroundColor: WidgetStateProperty.resolveWith( 207 | (states) { 208 | if (states.contains(WidgetState.disabled) && 209 | widget.options.disabledColor != null) { 210 | return widget.options.disabledColor; 211 | } 212 | if (states.contains(WidgetState.hovered) && 213 | widget.options.hoverColor != null) { 214 | return widget.options.hoverColor; 215 | } 216 | return widget.options.color; 217 | }, 218 | ), 219 | overlayColor: WidgetStateProperty.resolveWith((states) { 220 | if (states.contains(WidgetState.pressed)) { 221 | return widget.options.splashColor; 222 | } 223 | return widget.options.hoverColor == null ? null : Colors.transparent; 224 | }), 225 | padding: WidgetStateProperty.all(widget.options.padding ?? 226 | const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0)), 227 | elevation: WidgetStateProperty.resolveWith( 228 | (states) { 229 | if (states.contains(WidgetState.hovered) && 230 | widget.options.hoverElevation != null) { 231 | return widget.options.hoverElevation!; 232 | } 233 | return widget.options.elevation ?? 2.0; 234 | }, 235 | ), 236 | ); 237 | 238 | if ((widget.icon != null || widget.iconData != null) && !loading) { 239 | Widget icon = widget.icon ?? 240 | FaIcon( 241 | widget.iconData!, 242 | size: widget.options.iconSize, 243 | color: widget.options.iconColor, 244 | ); 245 | 246 | if (text == null) { 247 | return Container( 248 | height: widget.options.height, 249 | width: widget.options.width, 250 | decoration: BoxDecoration( 251 | border: Border.fromBorderSide( 252 | widget.options.borderSide ?? BorderSide.none, 253 | ), 254 | borderRadius: 255 | widget.options.borderRadius ?? BorderRadius.circular(8), 256 | ), 257 | child: IconButton( 258 | splashRadius: 1.0, 259 | icon: Padding( 260 | padding: widget.options.iconPadding ?? EdgeInsets.zero, 261 | child: icon, 262 | ), 263 | onPressed: onPressed, 264 | style: style, 265 | ), 266 | ); 267 | } 268 | return SizedBox( 269 | height: widget.options.height, 270 | width: widget.options.width, 271 | child: ElevatedButton.icon( 272 | icon: Padding( 273 | padding: widget.options.iconPadding ?? EdgeInsets.zero, 274 | child: icon, 275 | ), 276 | label: textWidget, 277 | onPressed: onPressed, 278 | style: style, 279 | ), 280 | ); 281 | } 282 | 283 | return SizedBox( 284 | height: widget.options.height, 285 | width: widget.options.width, 286 | child: ElevatedButton( 287 | onPressed: onPressed, 288 | style: style, 289 | child: textWidget, 290 | ), 291 | ); 292 | } 293 | } 294 | 295 | /// Extension on [TextStyle] to create a new [TextStyle] without the color property. 296 | /// 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. 297 | /// 298 | /// Example usage: 299 | /// ```dart 300 | /// TextStyle myTextStyle = TextStyle(color: Colors.red, fontSize: 16); 301 | /// TextStyle newTextStyle = myTextStyle.withoutColor(); 302 | /// ``` 303 | extension _WithoutColorExtension on TextStyle { 304 | /// Returns a new [TextStyle] object without the color property. 305 | TextStyle withoutColor() => TextStyle( 306 | inherit: inherit, 307 | color: null, 308 | backgroundColor: backgroundColor, 309 | fontSize: fontSize, 310 | fontWeight: fontWeight, 311 | fontStyle: fontStyle, 312 | letterSpacing: letterSpacing, 313 | wordSpacing: wordSpacing, 314 | textBaseline: textBaseline, 315 | height: height, 316 | leadingDistribution: leadingDistribution, 317 | locale: locale, 318 | foreground: foreground, 319 | background: background, 320 | shadows: shadows, 321 | fontFeatures: fontFeatures, 322 | decoration: decoration, 323 | decorationColor: decorationColor, 324 | decorationStyle: decorationStyle, 325 | decorationThickness: decorationThickness, 326 | debugLabel: debugLabel, 327 | fontFamily: fontFamily, 328 | fontFamilyFallback: fontFamilyFallback, 329 | overflow: overflow, 330 | ); 331 | } 332 | 333 | // Slightly hacky method of getting the layout width of the provided text. 334 | double? _getTextWidth(String? text, TextStyle? style, int maxLines) => 335 | text != null 336 | ? (TextPainter( 337 | text: TextSpan(text: text, style: style), 338 | textDirection: TextDirection.ltr, 339 | maxLines: maxLines, 340 | )..layout()) 341 | .size 342 | .width 343 | : null; 344 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_data_table.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:data_table_2/data_table_2.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | export 'package:data_table_2/data_table_2.dart' show DataColumn2; 7 | 8 | const _kDataTableHorizontalMargin = 48.0; 9 | const kDefaultColumnSpacing = 56.0; 10 | const _kMinRowsPerPage = 5; 11 | 12 | typedef ColumnsBuilder = List Function(void Function(int, bool)); 13 | typedef DataRowBuilder = DataRow? Function( 14 | T, int, bool, void Function(bool?)?); 15 | 16 | class FlutterFlowDataTableController extends DataTableSource { 17 | FlutterFlowDataTableController({ 18 | List? initialData, 19 | int? numRows, 20 | PaginatorController? paginatorController, 21 | bool selectable = false, 22 | }) { 23 | data = initialData?.toList() ?? []; 24 | numRows = numRows; 25 | this.paginatorController = paginatorController ?? PaginatorController(); 26 | _selectable = selectable; 27 | } 28 | 29 | DataRowBuilder? _dataRowBuilder; 30 | late PaginatorController paginatorController; 31 | List data = []; 32 | int? _numRows; 33 | List get selectedData => 34 | selectedRows.where((i) => i < data.length).map(data.elementAt).toList(); 35 | 36 | bool _selectable = false; 37 | final Set selectedRows = {}; 38 | 39 | int rowsPerPage = defaultRowsPerPage; 40 | int? sortColumnIndex; 41 | bool sortAscending = true; 42 | 43 | void init({ 44 | DataRowBuilder? dataRowBuilder, 45 | bool? selectable, 46 | List? initialData, 47 | int? initialNumRows, 48 | }) { 49 | _dataRowBuilder = dataRowBuilder ?? _dataRowBuilder; 50 | _selectable = selectable ?? _selectable; 51 | data = initialData?.toList() ?? data; 52 | _numRows = initialNumRows; 53 | } 54 | 55 | void updateData({ 56 | List? data, 57 | int? numRows, 58 | bool notify = true, 59 | }) { 60 | this.data = data?.toList() ?? this.data; 61 | _numRows = numRows ?? _numRows; 62 | if (notify) { 63 | notifyListeners(); 64 | } 65 | } 66 | 67 | void updateSort({ 68 | required int columnIndex, 69 | required bool ascending, 70 | Function(int, bool)? onSortChanged, 71 | }) { 72 | sortColumnIndex = columnIndex; 73 | sortAscending = ascending; 74 | if (onSortChanged != null) { 75 | onSortChanged(columnIndex, ascending); 76 | } 77 | notifyListeners(); 78 | } 79 | 80 | @override 81 | DataRow? getRow(int index) { 82 | final row = data.elementAtOrNull(index); 83 | return _dataRowBuilder != null && row != null 84 | ? _dataRowBuilder!( 85 | row, 86 | index, 87 | selectedRows.contains(index), 88 | _selectable 89 | ? (selected) { 90 | if (selected == null) { 91 | return; 92 | } 93 | selected 94 | ? selectedRows.add(index) 95 | : selectedRows.remove(index); 96 | notifyListeners(); 97 | } 98 | : null, 99 | ) 100 | : null; 101 | } 102 | 103 | @override 104 | bool get isRowCountApproximate => false; 105 | 106 | @override 107 | int get rowCount => _numRows ?? data.length; 108 | 109 | @override 110 | int get selectedRowCount => selectedRows.length; 111 | } 112 | 113 | /// A widget that displays tabular data in a grid format. 114 | class FlutterFlowDataTable extends StatefulWidget { 115 | const FlutterFlowDataTable({ 116 | super.key, 117 | required this.controller, 118 | required this.data, 119 | this.numRows, 120 | required this.columnsBuilder, 121 | required this.dataRowBuilder, 122 | this.emptyBuilder, 123 | this.onPageChanged, 124 | this.onSortChanged, 125 | this.onRowsPerPageChanged, 126 | this.paginated = true, 127 | this.selectable = false, 128 | this.hidePaginator = false, 129 | this.showFirstLastButtons = false, 130 | this.width, 131 | this.height, 132 | this.minWidth, 133 | this.headingRowHeight = 56, 134 | this.dataRowHeight = kMinInteractiveDimension, 135 | this.columnSpacing = kDefaultColumnSpacing, 136 | this.headingRowColor, 137 | this.sortIconColor, 138 | this.borderRadius, 139 | this.addHorizontalDivider = true, 140 | this.addTopAndBottomDivider = false, 141 | this.hideDefaultHorizontalDivider = false, 142 | this.addVerticalDivider = false, 143 | this.horizontalDividerColor, 144 | this.horizontalDividerThickness, 145 | this.verticalDividerColor, 146 | this.verticalDividerThickness, 147 | this.checkboxUnselectedFillColor, 148 | this.checkboxSelectedFillColor, 149 | this.checkboxUnselectedBorderColor, 150 | this.checkboxSelectedBorderColor, 151 | this.checkboxCheckColor, 152 | }); 153 | 154 | final FlutterFlowDataTableController controller; 155 | final List data; 156 | final int? numRows; 157 | final ColumnsBuilder columnsBuilder; 158 | final DataRowBuilder dataRowBuilder; 159 | final Widget? Function()? emptyBuilder; 160 | // Callback functions 161 | final Function(int)? onPageChanged; 162 | final Function(int, bool)? onSortChanged; 163 | final Function(int)? onRowsPerPageChanged; 164 | // Functionality options 165 | final bool paginated; 166 | final bool selectable; 167 | final bool showFirstLastButtons; 168 | final bool hidePaginator; 169 | // Size and shape options 170 | final double? width; 171 | final double? height; 172 | final double? minWidth; 173 | final double headingRowHeight; 174 | final double dataRowHeight; 175 | final double columnSpacing; 176 | // Table style options 177 | final Color? headingRowColor; 178 | final Color? sortIconColor; 179 | final BorderRadius? borderRadius; 180 | final bool addHorizontalDivider; 181 | final bool addTopAndBottomDivider; 182 | final bool hideDefaultHorizontalDivider; 183 | final Color? horizontalDividerColor; 184 | final double? horizontalDividerThickness; 185 | final bool addVerticalDivider; 186 | final Color? verticalDividerColor; 187 | final double? verticalDividerThickness; 188 | // Checkbox style options 189 | final Color? checkboxUnselectedFillColor; 190 | final Color? checkboxSelectedFillColor; 191 | final Color? checkboxUnselectedBorderColor; 192 | final Color? checkboxSelectedBorderColor; 193 | final Color? checkboxCheckColor; 194 | 195 | @override 196 | State> createState() => 197 | _FlutterFlowDataTableState(); 198 | } 199 | 200 | class _FlutterFlowDataTableState extends State> { 201 | FlutterFlowDataTableController get controller => widget.controller; 202 | int get rowCount => controller.rowCount; 203 | 204 | int get initialRowsPerPage => 205 | rowCount > _kMinRowsPerPage ? defaultRowsPerPage : _kMinRowsPerPage; 206 | 207 | @override 208 | void initState() { 209 | super.initState(); 210 | dataTableShowLogs = false; // Disables noisy DataTable2 debug statements. 211 | controller.init( 212 | dataRowBuilder: widget.dataRowBuilder, 213 | selectable: widget.selectable, 214 | initialData: widget.data, 215 | initialNumRows: widget.numRows, 216 | ); 217 | // ignore: cascade_invocations 218 | controller.addListener(() => setState(() {})); 219 | } 220 | 221 | @override 222 | void didUpdateWidget(FlutterFlowDataTable oldWidget) { 223 | super.didUpdateWidget(oldWidget); 224 | controller.updateData( 225 | data: widget.data, 226 | numRows: widget.numRows, 227 | notify: true, 228 | ); 229 | } 230 | 231 | @override 232 | Widget build(BuildContext context) { 233 | final columns = widget.columnsBuilder( 234 | (index, ascending) { 235 | controller.updateSort( 236 | columnIndex: index, 237 | ascending: ascending, 238 | onSortChanged: widget.onSortChanged, 239 | ); 240 | setState(() {}); 241 | }, 242 | ); 243 | 244 | final checkboxThemeData = CheckboxThemeData( 245 | checkColor: WidgetStateProperty.all( 246 | widget.checkboxCheckColor ?? Colors.black54, 247 | ), 248 | fillColor: WidgetStateProperty.resolveWith( 249 | (states) => states.contains(WidgetState.selected) 250 | ? widget.checkboxSelectedFillColor ?? Colors.white.withOpacity(0.01) 251 | : widget.checkboxUnselectedFillColor ?? 252 | Colors.white.withOpacity(0.01), 253 | ), 254 | side: WidgetStateBorderSide.resolveWith( 255 | (states) => BorderSide( 256 | width: 2.0, 257 | color: states.contains(WidgetState.selected) 258 | ? widget.checkboxSelectedBorderColor ?? Colors.black54 259 | : widget.checkboxUnselectedBorderColor ?? Colors.black54, 260 | ), 261 | ), 262 | overlayColor: WidgetStateProperty.all(Colors.transparent), 263 | ); 264 | 265 | final horizontalBorder = widget.addHorizontalDivider 266 | ? BorderSide( 267 | color: widget.horizontalDividerColor ?? Colors.transparent, 268 | width: widget.horizontalDividerThickness ?? 1.0, 269 | ) 270 | : BorderSide.none; 271 | 272 | return ClipRRect( 273 | borderRadius: widget.borderRadius ?? BorderRadius.zero, 274 | child: SizedBox( 275 | width: widget.width, 276 | height: widget.height, 277 | child: Theme( 278 | data: Theme.of(context).copyWith( 279 | iconTheme: widget.sortIconColor != null 280 | ? IconThemeData(color: widget.sortIconColor) 281 | : null, 282 | ), 283 | child: PaginatedDataTable2( 284 | source: controller, 285 | controller: 286 | widget.paginated ? controller.paginatorController : null, 287 | rowsPerPage: widget.paginated ? initialRowsPerPage : rowCount, 288 | availableRowsPerPage: const [5, 10, 25, 50, 100], 289 | onPageChanged: widget.onPageChanged != null 290 | ? (index) => widget.onPageChanged!(index) 291 | : null, 292 | columnSpacing: widget.columnSpacing, 293 | onRowsPerPageChanged: widget.paginated 294 | ? (value) { 295 | controller.rowsPerPage = value ?? initialRowsPerPage; 296 | if (widget.onRowsPerPageChanged != null) { 297 | widget.onRowsPerPageChanged!(controller.rowsPerPage); 298 | } 299 | } 300 | : null, 301 | columns: columns, 302 | empty: widget.emptyBuilder != null ? widget.emptyBuilder!() : null, 303 | sortColumnIndex: controller.sortColumnIndex, 304 | sortAscending: controller.sortAscending, 305 | showCheckboxColumn: widget.selectable, 306 | datarowCheckboxTheme: checkboxThemeData, 307 | headingCheckboxTheme: checkboxThemeData, 308 | hidePaginator: !widget.paginated || widget.hidePaginator, 309 | wrapInCard: false, 310 | renderEmptyRowsInTheEnd: false, 311 | border: TableBorder( 312 | horizontalInside: horizontalBorder, 313 | verticalInside: widget.addVerticalDivider 314 | ? BorderSide( 315 | color: widget.verticalDividerColor ?? Colors.transparent, 316 | width: widget.verticalDividerThickness ?? 1.0, 317 | ) 318 | : BorderSide.none, 319 | bottom: widget.addTopAndBottomDivider 320 | ? horizontalBorder 321 | : BorderSide.none, 322 | ), 323 | dividerThickness: widget.hideDefaultHorizontalDivider ? 0.0 : null, 324 | headingRowColor: WidgetStateProperty.all(widget.headingRowColor), 325 | headingRowHeight: widget.headingRowHeight, 326 | dataRowHeight: widget.dataRowHeight, 327 | showFirstLastButtons: widget.showFirstLastButtons, 328 | minWidth: math.max(widget.minWidth ?? 0, _getColumnsWidth(columns)), 329 | ), 330 | ), 331 | ), 332 | ); 333 | } 334 | 335 | // Return the total fixed width of all columns that have a specified width, 336 | // plus one to make the data table scrollable if there is insufficient space. 337 | double _getColumnsWidth(List columns) => 338 | columns.where((c) => c is DataColumn2 && c.fixedWidth != null).fold( 339 | ((widget.selectable ? 2 : 1) * _kDataTableHorizontalMargin) + 1, 340 | (sum, col) => sum + (col as DataColumn2).fixedWidth!, 341 | ); 342 | } 343 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_credit_card_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_credit_card/flutter_credit_card.dart'; 3 | // ignore: implementation_imports 4 | import 'package:flutter_credit_card/src/masked_text_controller.dart'; 5 | 6 | export 'package:flutter_credit_card/flutter_credit_card.dart' 7 | show CreditCardModel; 8 | 9 | /// Modified from https://pub.dev/packages/flutter_credit_card (see license below) 10 | 11 | CreditCardModel emptyCreditCard() => CreditCardModel('', '', '', '', false); 12 | 13 | /// A form widget for entering credit card information. 14 | class FlutterFlowCreditCardForm extends StatefulWidget { 15 | /// Creates a [FlutterFlowCreditCardForm]. 16 | /// 17 | /// - [formKey] is a global key that uniquely identifies the form. 18 | /// - [creditCardModel] is the model that holds the credit card information. 19 | /// - [obscureNumber] determines whether the credit card number should be obscured. 20 | /// - [obscureCvv] determines whether the CVV should be obscured. 21 | /// - [textStyle] is the style of the text in the form. 22 | /// - [spacing] is the spacing between form fields. 23 | /// - [inputDecoration] is the decoration for the form fields. 24 | const FlutterFlowCreditCardForm({ 25 | super.key, 26 | required this.formKey, 27 | required this.creditCardModel, 28 | this.obscureNumber = false, 29 | this.obscureCvv = false, 30 | this.textStyle, 31 | this.spacing = 10.0, 32 | this.inputDecoration = const InputDecoration( 33 | border: OutlineInputBorder(), 34 | ), 35 | }); 36 | 37 | final GlobalKey formKey; 38 | final CreditCardModel creditCardModel; 39 | final bool obscureNumber; 40 | final bool obscureCvv; 41 | final TextStyle? textStyle; 42 | final double spacing; 43 | final InputDecoration inputDecoration; 44 | 45 | @override 46 | State createState() => 47 | _FlutterFlowCreditCardFormState(); 48 | } 49 | 50 | class _FlutterFlowCreditCardFormState extends State { 51 | final TextEditingController _cardNumberController = 52 | MaskedTextController(mask: '0000 0000 0000 0000'); 53 | final TextEditingController _expiryDateController = 54 | MaskedTextController(mask: '00/00'); 55 | final TextEditingController _cvvCodeController = 56 | MaskedTextController(mask: '0000'); 57 | 58 | FocusNode cvvFocusNode = FocusNode(); 59 | FocusNode cardNumberNode = FocusNode(); 60 | FocusNode expiryDateNode = FocusNode(); 61 | 62 | String get cardNumber => widget.creditCardModel.cardNumber; 63 | 64 | void textFieldFocusDidChange() { 65 | widget.creditCardModel.isCvvFocused = cvvFocusNode.hasFocus; 66 | } 67 | 68 | @override 69 | void initState() { 70 | super.initState(); 71 | if (widget.creditCardModel.cardNumber.isNotEmpty) { 72 | _cardNumberController.text = widget.creditCardModel.cardNumber; 73 | } 74 | if (widget.creditCardModel.expiryDate.isNotEmpty) { 75 | _expiryDateController.text = widget.creditCardModel.expiryDate; 76 | } 77 | if (widget.creditCardModel.cvvCode.isNotEmpty) { 78 | _cvvCodeController.text = widget.creditCardModel.cvvCode; 79 | } 80 | cvvFocusNode.addListener(textFieldFocusDidChange); 81 | _cardNumberController.addListener(() => setState( 82 | () => widget.creditCardModel.cardNumber = _cardNumberController.text)); 83 | _expiryDateController.addListener(() => setState( 84 | () => widget.creditCardModel.expiryDate = _expiryDateController.text)); 85 | _cvvCodeController.addListener(() => setState( 86 | () => widget.creditCardModel.cvvCode = _cvvCodeController.text)); 87 | } 88 | 89 | @override 90 | void dispose() { 91 | cvvFocusNode.dispose(); 92 | expiryDateNode.dispose(); 93 | super.dispose(); 94 | } 95 | 96 | @override 97 | Widget build(BuildContext context) => Form( 98 | key: widget.formKey, 99 | child: Column( 100 | children: [ 101 | Container( 102 | margin: const EdgeInsets.only(top: 12.0), 103 | child: TextFormField( 104 | obscureText: widget.obscureNumber, 105 | controller: _cardNumberController, 106 | onEditingComplete: () => 107 | FocusScope.of(context).requestFocus(expiryDateNode), 108 | style: widget.textStyle, 109 | decoration: widget.inputDecoration.copyWith( 110 | labelText: 'Card number', 111 | hintText: 'XXXX XXXX XXXX XXXX', 112 | labelStyle: widget.textStyle, 113 | hintStyle: widget.textStyle, 114 | ), 115 | keyboardType: TextInputType.number, 116 | textInputAction: TextInputAction.next, 117 | validator: (value) { 118 | // Validate less that 13 digits +3 white spaces 119 | if (value == null || value.isEmpty || value.length < 16) { 120 | return 'Please input a valid number'; 121 | } 122 | return null; 123 | }, 124 | ), 125 | ), 126 | SizedBox(height: widget.spacing), 127 | Row( 128 | crossAxisAlignment: CrossAxisAlignment.start, 129 | children: [ 130 | Expanded( 131 | child: Container( 132 | margin: const EdgeInsets.only(top: 8.0, bottom: 8.0), 133 | child: TextFormField( 134 | controller: _expiryDateController, 135 | focusNode: expiryDateNode, 136 | onEditingComplete: () { 137 | FocusScope.of(context).requestFocus(cvvFocusNode); 138 | }, 139 | style: widget.textStyle, 140 | decoration: widget.inputDecoration.copyWith( 141 | labelText: 'Exp. Date', 142 | hintText: 'MM/YY', 143 | labelStyle: widget.textStyle, 144 | hintStyle: widget.textStyle, 145 | ), 146 | keyboardType: TextInputType.number, 147 | textInputAction: TextInputAction.next, 148 | validator: (value) { 149 | if (value == null || value.isEmpty) { 150 | return 'Please input a valid date'; 151 | } 152 | 153 | final DateTime now = DateTime.now(); 154 | final List date = value.split(RegExp(r'/')); 155 | final int month = int.parse(date.first); 156 | final int year = int.parse('20\${date.last}'); 157 | final DateTime cardDate = DateTime(year, month); 158 | 159 | if (cardDate.isBefore(now) || 160 | month > 12 || 161 | month == 0) { 162 | return 'Please input a valid date'; 163 | } 164 | return null; 165 | }, 166 | ), 167 | ), 168 | ), 169 | const SizedBox(width: 16.0), 170 | Expanded( 171 | child: Container( 172 | margin: const EdgeInsets.only(top: 8.0, bottom: 8.0), 173 | child: TextFormField( 174 | obscureText: widget.obscureCvv, 175 | focusNode: cvvFocusNode, 176 | controller: _cvvCodeController, 177 | style: widget.textStyle, 178 | decoration: widget.inputDecoration.copyWith( 179 | labelText: 'CVV', 180 | hintText: 'XXX', 181 | labelStyle: widget.textStyle, 182 | hintStyle: widget.textStyle, 183 | ), 184 | keyboardType: TextInputType.number, 185 | textInputAction: TextInputAction.next, 186 | validator: (value) { 187 | if (value == null || 188 | value.isEmpty || 189 | value.length < 3) { 190 | return 'Please input a valid CVV'; 191 | } 192 | return null; 193 | }, 194 | ), 195 | ), 196 | ), 197 | ], 198 | ), 199 | ], 200 | ), 201 | ); 202 | 203 | /// Credit Card prefix patterns as of March 2019 204 | /// A [List] represents a range. 205 | /// i.e. ['51', '55'] represents the range of cards starting with '51' to those starting with '55' 206 | Map>> cardNumPatterns = 207 | >>{ 208 | CardType.visa: >{ 209 | ['4'], 210 | }, 211 | CardType.americanExpress: >{ 212 | ['34'], 213 | ['37'], 214 | }, 215 | CardType.discover: >{ 216 | ['6011'], 217 | ['622126', '622925'], 218 | ['644', '649'], 219 | ['65'] 220 | }, 221 | CardType.mastercard: >{ 222 | ['51', '55'], 223 | ['2221', '2229'], 224 | ['223', '229'], 225 | ['23', '26'], 226 | ['270', '271'], 227 | ['2720'], 228 | }, 229 | }; 230 | 231 | /// This function determines the Credit Card type based on the cardPatterns 232 | /// and returns it. 233 | CardType detectCCType(String cardNumber) { 234 | //Default card type is other 235 | CardType cardType = CardType.otherBrand; 236 | 237 | if (cardNumber.isEmpty) { 238 | return cardType; 239 | } 240 | 241 | cardNumPatterns.forEach( 242 | (type, patterns) { 243 | for (final patternRange in patterns) { 244 | // Remove any spaces 245 | String ccPatternStr = cardNumber.replaceAll(RegExp(r's+\b|\bs'), ''); 246 | final int rangeLen = patternRange[0].length; 247 | // Trim the Credit Card number string to match the pattern prefix length 248 | if (rangeLen < cardNumber.length) { 249 | ccPatternStr = ccPatternStr.substring(0, rangeLen); 250 | } 251 | 252 | if (patternRange.length > 1) { 253 | // Convert the prefix range into numbers then make sure the 254 | // Credit Card num is in the pattern range. 255 | // Because Strings don't have '>=' type operators 256 | final int ccPrefixAsInt = int.parse(ccPatternStr); 257 | final int startPatternPrefixAsInt = int.parse(patternRange[0]); 258 | final int endPatternPrefixAsInt = int.parse(patternRange[1]); 259 | if (ccPrefixAsInt >= startPatternPrefixAsInt && 260 | ccPrefixAsInt <= endPatternPrefixAsInt) { 261 | // Found a match 262 | cardType = type; 263 | break; 264 | } 265 | } else { 266 | // Just compare the single pattern prefix with the Credit Card prefix 267 | if (ccPatternStr == patternRange[0]) { 268 | // Found a match 269 | cardType = type; 270 | break; 271 | } 272 | } 273 | } 274 | }, 275 | ); 276 | 277 | return cardType; 278 | } 279 | } 280 | 281 | /// BSD 2-Clause License 282 | 283 | /// Copyright (c) 2019, Simform Solutions 284 | /// All rights reserved. 285 | 286 | /// Redistribution and use in source and binary forms, with or without 287 | /// modification, are permitted provided that the following conditions are met: 288 | 289 | /// 1. Redistributions of source code must retain the above copyright notice, this 290 | /// list of conditions and the following disclaimer. 291 | 292 | /// 2. Redistributions in binary form must reproduce the above copyright notice, 293 | /// this list of conditions and the following disclaimer in the documentation 294 | /// and/or other materials provided with the distribution. 295 | 296 | /// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 297 | /// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 298 | /// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 299 | /// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 300 | /// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 301 | /// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 302 | /// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 303 | /// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 304 | /// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 305 | /// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_calendar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:table_calendar/table_calendar.dart'; 5 | 6 | DateTime kFirstDay = DateTime(1970, 1, 1); 7 | DateTime kLastDay = DateTime(2100, 1, 1); 8 | 9 | extension DateTimeExtension on DateTime { 10 | DateTime get startOfDay => DateTime(year, month, day); 11 | 12 | DateTime get endOfDay => DateTime(year, month, day, 23, 59); 13 | } 14 | 15 | /// A customizable calendar widget for FlutterFlow. 16 | /// 17 | /// The `FlutterFlowCalendar` widget allows you to display a calendar with various customization options. 18 | /// You can customize the color, date format, starting day of the week, header style, and more. 19 | /// 20 | /// To use this widget, simply create an instance of `FlutterFlowCalendar` and pass in the desired parameters. 21 | /// You can also provide a callback function to handle changes in the selected date range. 22 | /// 23 | /// Example usage: 24 | /// ```dart 25 | /// FlutterFlowCalendar( 26 | /// color: Colors.blue, 27 | /// onChange: (DateTimeRange? selectedRange) { 28 | /// // Handle selected date range change 29 | /// }, 30 | /// initialDate: DateTime.now(), 31 | /// weekFormat: true, 32 | /// weekStartsMonday: true, 33 | /// twoRowHeader: true, 34 | /// iconColor: Colors.white, 35 | /// dateStyle: TextStyle(fontSize: 16), 36 | /// dayOfWeekStyle: TextStyle(fontWeight: FontWeight.bold), 37 | /// inactiveDateStyle: TextStyle(color: Colors.grey), 38 | /// selectedDateStyle: TextStyle(color: Colors.red), 39 | /// titleStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), 40 | /// rowHeight: 40, 41 | /// locale: 'en_US', 42 | /// ) 43 | /// ``` 44 | class FlutterFlowCalendar extends StatefulWidget { 45 | /// Creates a new instance of [FlutterFlowCalendar]. 46 | /// 47 | /// - `color` parameter specifies the background color of the calendar. 48 | /// - `onChange` parameter is a callback function that will be called when the selected date range changes. 49 | /// - `initialDate` parameter specifies the initial date to be displayed on the calendar. 50 | /// - `weekFormat` parameter determines whether the calendar should be displayed in week format. 51 | /// - `weekStartsMonday` parameter determines whether the week starts on Monday. 52 | /// - `twoRowHeader` parameter determines whether the header should be displayed in two rows. 53 | /// - `iconColor` parameter specifies the color of the icons used in the calendar. 54 | /// - `dateStyle` parameter specifies the text style for the dates. 55 | /// - `dayOfWeekStyle` parameter specifies the text style for the day of the week labels. 56 | /// - `inactiveDateStyle` parameter specifies the text style for inactive dates. 57 | /// - `selectedDateStyle` parameter specifies the text style for selected dates. 58 | /// - `titleStyle` parameter specifies the text style for the calendar title. 59 | /// - `rowHeight` parameter specifies the height of each row in the calendar. 60 | /// - `locale` parameter specifies the locale to be used for formatting dates. 61 | const FlutterFlowCalendar({ 62 | super.key, 63 | required this.color, 64 | this.onChange, 65 | this.initialDate, 66 | this.weekFormat = false, 67 | this.weekStartsMonday = false, 68 | this.twoRowHeader = false, 69 | this.iconColor, 70 | this.dateStyle, 71 | this.dayOfWeekStyle, 72 | this.inactiveDateStyle, 73 | this.selectedDateStyle, 74 | this.titleStyle, 75 | this.rowHeight, 76 | this.locale, 77 | }); 78 | 79 | /// Determines whether the calendar should be displayed in week format. 80 | final bool weekFormat; 81 | 82 | /// Determines whether the week starts on Monday. 83 | final bool weekStartsMonday; 84 | 85 | /// Determines whether the header should have two rows. 86 | final bool twoRowHeader; 87 | 88 | /// The color of the calendar. 89 | final Color color; 90 | 91 | /// A callback function that is called when the selected date range changes. 92 | final void Function(DateTimeRange?)? onChange; 93 | 94 | /// The initial date to be displayed on the calendar. 95 | final DateTime? initialDate; 96 | 97 | /// The color of the icons in the calendar. 98 | final Color? iconColor; 99 | 100 | /// The text style for the dates displayed on the calendar. 101 | final TextStyle? dateStyle; 102 | 103 | /// The text style for the day of the week displayed on the calendar. 104 | final TextStyle? dayOfWeekStyle; 105 | 106 | /// The text style for the inactive dates on the calendar. 107 | final TextStyle? inactiveDateStyle; 108 | 109 | /// The text style for the selected dates on the calendar. 110 | final TextStyle? selectedDateStyle; 111 | 112 | /// The text style for the title of the calendar. 113 | final TextStyle? titleStyle; 114 | 115 | /// The height of each row in the calendar. 116 | final double? rowHeight; 117 | 118 | /// The locale to be used for formatting dates. 119 | final String? locale; 120 | 121 | @override 122 | State createState() => _FlutterFlowCalendarState(); 123 | } 124 | 125 | class _FlutterFlowCalendarState extends State { 126 | late DateTime focusedDay; 127 | late DateTime selectedDay; 128 | late DateTimeRange selectedRange; 129 | 130 | @override 131 | void initState() { 132 | super.initState(); 133 | focusedDay = widget.initialDate ?? DateTime.now(); 134 | selectedDay = widget.initialDate ?? DateTime.now(); 135 | selectedRange = DateTimeRange( 136 | start: selectedDay.startOfDay, 137 | end: selectedDay.endOfDay, 138 | ); 139 | SchedulerBinding.instance 140 | .addPostFrameCallback((_) => setSelectedDay(selectedRange.start)); 141 | } 142 | 143 | CalendarFormat get calendarFormat => 144 | widget.weekFormat ? CalendarFormat.week : CalendarFormat.month; 145 | 146 | StartingDayOfWeek get startingDayOfWeek => widget.weekStartsMonday 147 | ? StartingDayOfWeek.monday 148 | : StartingDayOfWeek.sunday; 149 | 150 | Color get color => widget.color; 151 | 152 | Color get lightColor => widget.color.withOpacity(0.85); 153 | 154 | Color get lighterColor => widget.color.withOpacity(0.60); 155 | 156 | void setSelectedDay( 157 | DateTime? newSelectedDay, [ 158 | DateTime? newSelectedEnd, 159 | ]) { 160 | final newRange = newSelectedDay == null 161 | ? null 162 | : DateTimeRange( 163 | start: newSelectedDay.startOfDay, 164 | end: newSelectedEnd ?? newSelectedDay.endOfDay, 165 | ); 166 | setState(() { 167 | selectedDay = newSelectedDay ?? selectedDay; 168 | selectedRange = newRange ?? selectedRange; 169 | if (widget.onChange != null) { 170 | widget.onChange!(newRange); 171 | } 172 | }); 173 | } 174 | 175 | @override 176 | Widget build(BuildContext context) => Column( 177 | crossAxisAlignment: CrossAxisAlignment.center, 178 | mainAxisAlignment: MainAxisAlignment.center, 179 | mainAxisSize: MainAxisSize.min, 180 | children: [ 181 | CalendarHeader( 182 | focusedDay: focusedDay, 183 | onLeftChevronTap: () => setState( 184 | () => focusedDay = widget.weekFormat 185 | ? _previousWeek(focusedDay) 186 | : _previousMonth(focusedDay), 187 | ), 188 | onRightChevronTap: () => setState( 189 | () => focusedDay = widget.weekFormat 190 | ? _nextWeek(focusedDay) 191 | : _nextMonth(focusedDay), 192 | ), 193 | onTodayButtonTap: () => setState(() => focusedDay = DateTime.now()), 194 | titleStyle: widget.titleStyle, 195 | iconColor: widget.iconColor, 196 | locale: widget.locale, 197 | twoRowHeader: widget.twoRowHeader, 198 | ), 199 | TableCalendar( 200 | focusedDay: focusedDay, 201 | selectedDayPredicate: (date) => isSameDay(selectedDay, date), 202 | firstDay: kFirstDay, 203 | lastDay: kLastDay, 204 | calendarFormat: calendarFormat, 205 | headerVisible: false, 206 | locale: widget.locale, 207 | rowHeight: widget.rowHeight ?? MediaQuery.sizeOf(context).width / 7, 208 | calendarStyle: CalendarStyle( 209 | defaultTextStyle: 210 | widget.dateStyle ?? const TextStyle(color: Color(0xFF5A5A5A)), 211 | weekendTextStyle: widget.dateStyle ?? 212 | const TextStyle(color: Color(0xFF5A5A5A)), 213 | holidayTextStyle: widget.dateStyle ?? 214 | const TextStyle(color: Color(0xFF5C6BC0)), 215 | selectedTextStyle: 216 | const TextStyle(color: Color(0xFFFAFAFA), fontSize: 16.0) 217 | .merge(widget.selectedDateStyle), 218 | todayTextStyle: 219 | const TextStyle(color: Color(0xFFFAFAFA), fontSize: 16.0) 220 | .merge(widget.selectedDateStyle), 221 | outsideTextStyle: const TextStyle(color: Color(0xFF9E9E9E)) 222 | .merge(widget.inactiveDateStyle), 223 | selectedDecoration: BoxDecoration( 224 | color: color, 225 | shape: BoxShape.circle, 226 | ), 227 | todayDecoration: BoxDecoration( 228 | color: lighterColor, 229 | shape: BoxShape.circle, 230 | ), 231 | markerDecoration: BoxDecoration( 232 | color: lightColor, 233 | shape: BoxShape.circle, 234 | ), 235 | markersMaxCount: 3, 236 | canMarkersOverflow: true, 237 | ), 238 | availableGestures: AvailableGestures.horizontalSwipe, 239 | startingDayOfWeek: startingDayOfWeek, 240 | daysOfWeekStyle: DaysOfWeekStyle( 241 | weekdayStyle: const TextStyle(color: Color(0xFF616161)) 242 | .merge(widget.dayOfWeekStyle), 243 | weekendStyle: const TextStyle(color: Color(0xFF616161)) 244 | .merge(widget.dayOfWeekStyle), 245 | ), 246 | onPageChanged: (focused) { 247 | if (focusedDay.startOfDay != focused.startOfDay) { 248 | setState(() => focusedDay = focused); 249 | } 250 | }, 251 | onDaySelected: (newSelectedDay, focused) { 252 | if (!isSameDay(selectedDay, newSelectedDay)) { 253 | setSelectedDay(newSelectedDay); 254 | if (focusedDay.startOfDay != focused.startOfDay) { 255 | setState(() => focusedDay = focused); 256 | } 257 | } 258 | }, 259 | ), 260 | ], 261 | ); 262 | } 263 | 264 | class CalendarHeader extends StatelessWidget { 265 | const CalendarHeader({ 266 | super.key, 267 | required this.focusedDay, 268 | required this.onLeftChevronTap, 269 | required this.onRightChevronTap, 270 | required this.onTodayButtonTap, 271 | this.iconColor, 272 | this.titleStyle, 273 | this.locale, 274 | this.twoRowHeader = false, 275 | }); 276 | 277 | final DateTime focusedDay; 278 | final VoidCallback onLeftChevronTap; 279 | final VoidCallback onRightChevronTap; 280 | final VoidCallback onTodayButtonTap; 281 | final Color? iconColor; 282 | final TextStyle? titleStyle; 283 | final String? locale; 284 | final bool twoRowHeader; 285 | 286 | @override 287 | Widget build(BuildContext context) => Container( 288 | decoration: const BoxDecoration(), 289 | margin: const EdgeInsets.all(0), 290 | padding: const EdgeInsets.symmetric(vertical: 8), 291 | child: twoRowHeader ? _buildTwoRowHeader() : _buildOneRowHeader(), 292 | ); 293 | 294 | Widget _buildTwoRowHeader() => Column( 295 | children: [ 296 | Row( 297 | mainAxisSize: MainAxisSize.max, 298 | children: [ 299 | const SizedBox(width: 16), 300 | _buildDateWidget(), 301 | ], 302 | ), 303 | Row( 304 | mainAxisSize: MainAxisSize.max, 305 | mainAxisAlignment: MainAxisAlignment.end, 306 | children: _buildCustomIconButtons(), 307 | ), 308 | ], 309 | ); 310 | 311 | Widget _buildOneRowHeader() => Row( 312 | mainAxisSize: MainAxisSize.max, 313 | children: [ 314 | const SizedBox(width: 16), 315 | _buildDateWidget(), 316 | ..._buildCustomIconButtons(), 317 | ], 318 | ); 319 | 320 | Widget _buildDateWidget() => Expanded( 321 | child: Text( 322 | DateFormat.yMMMM(locale).format(focusedDay), 323 | style: const TextStyle(fontSize: 17).merge(titleStyle), 324 | ), 325 | ); 326 | 327 | List _buildCustomIconButtons() => [ 328 | CustomIconButton( 329 | icon: Icon(Icons.calendar_today, color: iconColor), 330 | onTap: onTodayButtonTap, 331 | ), 332 | CustomIconButton( 333 | icon: Icon(Icons.chevron_left, color: iconColor), 334 | onTap: onLeftChevronTap, 335 | ), 336 | CustomIconButton( 337 | icon: Icon(Icons.chevron_right, color: iconColor), 338 | onTap: onRightChevronTap, 339 | ), 340 | ]; 341 | } 342 | 343 | class CustomIconButton extends StatelessWidget { 344 | const CustomIconButton({ 345 | super.key, 346 | required this.icon, 347 | required this.onTap, 348 | this.margin = const EdgeInsets.symmetric(horizontal: 4), 349 | this.padding = const EdgeInsets.all(10), 350 | }); 351 | 352 | final Icon icon; 353 | final VoidCallback onTap; 354 | final EdgeInsetsGeometry margin; 355 | final EdgeInsetsGeometry padding; 356 | 357 | @override 358 | Widget build(BuildContext context) => Padding( 359 | padding: margin, 360 | child: InkWell( 361 | onTap: onTap, 362 | borderRadius: BorderRadius.circular(100), 363 | child: Padding( 364 | padding: padding, 365 | child: Icon( 366 | icon.icon, 367 | color: icon.color, 368 | size: icon.size, 369 | ), 370 | ), 371 | ), 372 | ); 373 | } 374 | 375 | DateTime _previousWeek(DateTime week) { 376 | return week.subtract(const Duration(days: 7)); 377 | } 378 | 379 | DateTime _nextWeek(DateTime week) { 380 | return week.add(const Duration(days: 7)); 381 | } 382 | 383 | DateTime _previousMonth(DateTime month) { 384 | if (month.month == 1) { 385 | return DateTime(month.year - 1, 12); 386 | } else { 387 | return DateTime(month.year, month.month - 1); 388 | } 389 | } 390 | 391 | DateTime _nextMonth(DateTime month) { 392 | if (month.month == 12) { 393 | return DateTime(month.year + 1, 1); 394 | } else { 395 | return DateTime(month.year, month.month + 1); 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /lib/src/widgets/flutter_flow_drop_down.dart: -------------------------------------------------------------------------------- 1 | import 'package:dropdown_button2/dropdown_button2.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../utils/form_field_controller.dart'; 5 | 6 | /// A dropdown widget that allows the user to select an option from a list of options. 7 | class FlutterFlowDropDown extends StatefulWidget { 8 | const FlutterFlowDropDown({ 9 | super.key, 10 | this.controller, 11 | this.multiSelectController, 12 | this.hintText, 13 | this.searchHintText, 14 | required this.options, 15 | this.optionLabels, 16 | this.onChanged, 17 | this.onMultiSelectChanged, 18 | this.icon, 19 | this.width, 20 | this.height, 21 | this.maxHeight, 22 | this.fillColor, 23 | this.searchHintTextStyle, 24 | this.searchTextStyle, 25 | this.searchCursorColor, 26 | required this.textStyle, 27 | required this.elevation, 28 | required this.borderWidth, 29 | required this.borderRadius, 30 | required this.borderColor, 31 | required this.margin, 32 | this.hidesUnderline = false, 33 | this.disabled = false, 34 | this.isOverButton = false, 35 | this.menuOffset, 36 | this.isSearchable = false, 37 | this.isMultiSelect = false, 38 | this.labelText, 39 | this.labelTextStyle, 40 | }) : assert( 41 | isMultiSelect 42 | ? (controller == null && 43 | onChanged == null && 44 | multiSelectController != null && 45 | onMultiSelectChanged != null) 46 | : (controller != null && 47 | onChanged != null && 48 | multiSelectController == null && 49 | onMultiSelectChanged == null), 50 | ); 51 | 52 | /// The controller for the dropdown field. 53 | final FormFieldController? controller; 54 | 55 | /// The controller for the multi-select dropdown field. 56 | final FormFieldController?>? multiSelectController; 57 | 58 | /// The text to display as a hint when no option is selected. 59 | final String? hintText; 60 | 61 | /// The text to display as a hint in the search field. 62 | final String? searchHintText; 63 | 64 | /// The list of options to display in the dropdown. 65 | final List options; 66 | 67 | /// The list of labels corresponding to the options. 68 | final List? optionLabels; 69 | 70 | /// A callback function that is called when the selected option changes. 71 | final Function(T?)? onChanged; 72 | 73 | /// A callback function that is called when the selected options change in multi-select mode. 74 | final Function(List?)? onMultiSelectChanged; 75 | 76 | /// The icon to display in the dropdown field. 77 | final Widget? icon; 78 | 79 | /// The width of the dropdown field. 80 | final double? width; 81 | 82 | /// The height of the dropdown field. 83 | final double? height; 84 | 85 | /// The maximum height of the dropdown menu. 86 | final double? maxHeight; 87 | 88 | /// The background color of the dropdown field. 89 | final Color? fillColor; 90 | 91 | /// The text style for the search hint text. 92 | final TextStyle? searchHintTextStyle; 93 | 94 | /// The text style for the search text. 95 | final TextStyle? searchTextStyle; 96 | 97 | /// The color of the search cursor. 98 | final Color? searchCursorColor; 99 | 100 | /// The text style for the dropdown field. 101 | final TextStyle textStyle; 102 | 103 | /// The elevation of the dropdown menu. 104 | final double elevation; 105 | 106 | /// The width of the dropdown field's border. 107 | final double borderWidth; 108 | 109 | /// The border radius of the dropdown field. 110 | final double borderRadius; 111 | 112 | /// The color of the dropdown field's border. 113 | final Color borderColor; 114 | 115 | /// The margin around the dropdown field. 116 | final EdgeInsetsGeometry margin; 117 | 118 | /// Whether to hide the underline of the dropdown field. 119 | final bool hidesUnderline; 120 | 121 | /// Whether the dropdown is disabled. 122 | final bool disabled; 123 | 124 | /// Whether the dropdown menu is displayed over the button. 125 | final bool isOverButton; 126 | 127 | /// The offset of the dropdown menu. 128 | final Offset? menuOffset; 129 | 130 | /// Whether the dropdown is searchable. 131 | final bool isSearchable; 132 | 133 | /// Whether the dropdown is in multi-select mode. 134 | final bool isMultiSelect; 135 | 136 | /// The label text for the dropdown field. 137 | final String? labelText; 138 | 139 | /// The text style for the label text. 140 | final TextStyle? labelTextStyle; 141 | 142 | @override 143 | State> createState() => _FlutterFlowDropDownState(); 144 | } 145 | 146 | class _FlutterFlowDropDownState extends State> { 147 | bool get isMultiSelect => widget.isMultiSelect; 148 | FormFieldController get controller => widget.controller!; 149 | FormFieldController?> get multiSelectController => 150 | widget.multiSelectController!; 151 | 152 | T? get currentValue { 153 | final value = isMultiSelect 154 | ? multiSelectController.value?.firstOrNull 155 | : controller.value; 156 | return widget.options.contains(value) ? value : null; 157 | } 158 | 159 | Set get currentValues { 160 | if (!isMultiSelect || multiSelectController.value == null) { 161 | return {}; 162 | } 163 | return widget.options 164 | .toSet() 165 | .intersection(multiSelectController.value!.toSet()); 166 | } 167 | 168 | Map get optionLabels => Map.fromEntries( 169 | widget.options.asMap().entries.map( 170 | (option) => MapEntry( 171 | option.value, 172 | widget.optionLabels == null || 173 | widget.optionLabels!.length < option.key + 1 174 | ? option.value.toString() 175 | : widget.optionLabels![option.key], 176 | ), 177 | ), 178 | ); 179 | 180 | EdgeInsetsGeometry get horizontalMargin => widget.margin.clamp( 181 | EdgeInsetsDirectional.zero, 182 | const EdgeInsetsDirectional.symmetric(horizontal: double.infinity), 183 | ); 184 | 185 | late void Function() _listener; 186 | final TextEditingController _textEditingController = TextEditingController(); 187 | 188 | @override 189 | void initState() { 190 | super.initState(); 191 | if (isMultiSelect) { 192 | _listener = 193 | () => widget.onMultiSelectChanged!(multiSelectController.value); 194 | multiSelectController.addListener(_listener); 195 | } else { 196 | _listener = () => widget.onChanged!(controller.value); 197 | controller.addListener(_listener); 198 | } 199 | } 200 | 201 | @override 202 | void dispose() { 203 | if (isMultiSelect) { 204 | multiSelectController.removeListener(_listener); 205 | } else { 206 | controller.removeListener(_listener); 207 | } 208 | super.dispose(); 209 | } 210 | 211 | @override 212 | Widget build(BuildContext context) { 213 | final dropdownWidget = _buildDropdownWidget(); 214 | return SizedBox( 215 | width: widget.width, 216 | height: widget.height, 217 | child: DecoratedBox( 218 | decoration: BoxDecoration( 219 | borderRadius: BorderRadius.circular(widget.borderRadius), 220 | border: Border.all( 221 | color: widget.borderColor, 222 | width: widget.borderWidth, 223 | ), 224 | color: widget.fillColor, 225 | ), 226 | child: Padding( 227 | padding: _useDropdown2() ? EdgeInsets.zero : widget.margin, 228 | child: widget.hidesUnderline 229 | ? DropdownButtonHideUnderline(child: dropdownWidget) 230 | : dropdownWidget, 231 | ), 232 | ), 233 | ); 234 | } 235 | 236 | bool _useDropdown2() => 237 | widget.isMultiSelect || 238 | widget.isSearchable || 239 | !widget.isOverButton || 240 | widget.maxHeight != null; 241 | 242 | Widget _buildDropdownWidget() => 243 | _useDropdown2() ? _buildDropdown() : _buildLegacyDropdown(); 244 | 245 | Widget _buildLegacyDropdown() { 246 | return DropdownButtonFormField( 247 | value: currentValue, 248 | hint: _createHintText(), 249 | items: _createMenuItems(), 250 | elevation: widget.elevation.toInt(), 251 | onChanged: widget.disabled ? null : (value) => controller.value = value, 252 | icon: widget.icon, 253 | isExpanded: true, 254 | dropdownColor: widget.fillColor, 255 | focusColor: Colors.transparent, 256 | decoration: InputDecoration( 257 | labelText: widget.labelText == null || widget.labelText!.isEmpty 258 | ? null 259 | : widget.labelText, 260 | labelStyle: widget.labelTextStyle, 261 | border: widget.hidesUnderline 262 | ? InputBorder.none 263 | : const UnderlineInputBorder(), 264 | ), 265 | ); 266 | } 267 | 268 | Text? _createHintText() => widget.hintText != null 269 | ? Text(widget.hintText!, style: widget.textStyle) 270 | : null; 271 | 272 | List> _createMenuItems() => widget.options 273 | .map( 274 | (option) => DropdownMenuItem( 275 | value: option, 276 | child: Padding( 277 | padding: _useDropdown2() ? horizontalMargin : EdgeInsets.zero, 278 | child: Text(optionLabels[option] ?? '', style: widget.textStyle), 279 | ), 280 | ), 281 | ) 282 | .toList(); 283 | 284 | List> _createMultiselectMenuItems() => widget.options 285 | .map( 286 | (item) => DropdownMenuItem( 287 | value: item, 288 | // Disable default onTap to avoid closing menu when selecting an item 289 | enabled: false, 290 | child: StatefulBuilder( 291 | builder: (context, menuSetState) { 292 | final isSelected = 293 | multiSelectController.value?.contains(item) ?? false; 294 | return InkWell( 295 | onTap: () { 296 | multiSelectController.value ??= []; 297 | isSelected 298 | ? multiSelectController.value!.remove(item) 299 | : multiSelectController.value!.add(item); 300 | multiSelectController.update(); 301 | // This rebuilds the StatefulWidget to update the button's text. 302 | setState(() {}); 303 | // This rebuilds the dropdownMenu Widget to update the check mark. 304 | menuSetState(() {}); 305 | }, 306 | child: Container( 307 | height: double.infinity, 308 | padding: horizontalMargin, 309 | child: Row( 310 | children: [ 311 | if (isSelected) 312 | const Icon(Icons.check_box_outlined) 313 | else 314 | const Icon(Icons.check_box_outline_blank), 315 | const SizedBox(width: 16), 316 | Expanded( 317 | child: Text( 318 | optionLabels[item]!, 319 | style: widget.textStyle, 320 | ), 321 | ), 322 | ], 323 | ), 324 | ), 325 | ); 326 | }, 327 | ), 328 | ), 329 | ) 330 | .toList(); 331 | 332 | Widget _buildDropdown() { 333 | final overlayColor = WidgetStateProperty.resolveWith((states) => 334 | states.contains(WidgetState.focused) ? Colors.transparent : null); 335 | final iconStyleData = widget.icon != null 336 | ? IconStyleData(icon: widget.icon!) 337 | : const IconStyleData(); 338 | return DropdownButton2( 339 | value: currentValue, 340 | hint: _createHintText(), 341 | items: isMultiSelect ? _createMultiselectMenuItems() : _createMenuItems(), 342 | iconStyleData: iconStyleData, 343 | buttonStyleData: ButtonStyleData( 344 | elevation: widget.elevation.toInt(), 345 | overlayColor: overlayColor, 346 | padding: widget.margin, 347 | ), 348 | menuItemStyleData: MenuItemStyleData( 349 | overlayColor: overlayColor, 350 | padding: EdgeInsets.zero, 351 | ), 352 | dropdownStyleData: DropdownStyleData( 353 | elevation: widget.elevation.toInt(), 354 | decoration: BoxDecoration( 355 | borderRadius: BorderRadius.circular(4.0), 356 | color: widget.fillColor, 357 | ), 358 | isOverButton: widget.isOverButton, 359 | offset: widget.menuOffset ?? Offset.zero, 360 | maxHeight: widget.maxHeight, 361 | padding: EdgeInsets.zero, 362 | ), 363 | onChanged: widget.disabled 364 | ? null 365 | : (isMultiSelect ? (_) {} : (val) => widget.controller!.value = val), 366 | isExpanded: true, 367 | selectedItemBuilder: (context) => widget.options 368 | .map( 369 | (item) => Align( 370 | alignment: AlignmentDirectional.centerStart, 371 | child: Text( 372 | isMultiSelect 373 | ? currentValues 374 | .where((v) => optionLabels.containsKey(v)) 375 | .map((v) => optionLabels[v]) 376 | .join(', ') 377 | : optionLabels[item]!, 378 | style: widget.textStyle, 379 | maxLines: 1, 380 | ), 381 | ), 382 | ) 383 | .toList(), 384 | dropdownSearchData: widget.isSearchable 385 | ? DropdownSearchData( 386 | searchController: _textEditingController, 387 | searchInnerWidgetHeight: 50, 388 | searchInnerWidget: Container( 389 | height: 50, 390 | padding: const EdgeInsets.only( 391 | top: 8, 392 | bottom: 4, 393 | right: 8, 394 | left: 8, 395 | ), 396 | child: TextFormField( 397 | expands: true, 398 | maxLines: null, 399 | controller: _textEditingController, 400 | cursorColor: widget.searchCursorColor, 401 | style: widget.searchTextStyle, 402 | decoration: InputDecoration( 403 | isDense: true, 404 | contentPadding: const EdgeInsets.symmetric( 405 | horizontal: 10, 406 | vertical: 8, 407 | ), 408 | hintText: widget.searchHintText, 409 | hintStyle: widget.searchHintTextStyle, 410 | border: OutlineInputBorder( 411 | borderRadius: BorderRadius.circular(8), 412 | ), 413 | ), 414 | ), 415 | ), 416 | searchMatchFn: (item, searchValue) { 417 | return (optionLabels[item.value] ?? '') 418 | .toLowerCase() 419 | .contains(searchValue.toLowerCase()); 420 | }, 421 | ) 422 | : null, 423 | // This is to clear the search value when you close the menu 424 | onMenuStateChange: widget.isSearchable 425 | ? (isOpen) { 426 | if (!isOpen) { 427 | _textEditingController.clear(); 428 | } 429 | } 430 | : null, 431 | ); 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /.packages: -------------------------------------------------------------------------------- 1 | # This file is deprecated. Tools should instead consume 2 | # `.dart_tool/package_config.json`. 3 | # 4 | # For more info see: https://dart.dev/go/dot-packages-deprecation 5 | # 6 | # Generated by pub on 2022-06-14 17:29:42.696281. 7 | _fe_analyzer_shared:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-38.0.0/lib/ 8 | analyzer:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/analyzer-3.4.1/lib/ 9 | args:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/args-2.3.1/lib/ 10 | async:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/async-2.8.2/lib/ 11 | auto_size_text:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/auto_size_text-3.0.0/lib/ 12 | boolean_selector:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/boolean_selector-2.1.0/lib/ 13 | build:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/build-2.3.0/lib/ 14 | build_config:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/build_config-1.0.0/lib/ 15 | cached_network_image:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/cached_network_image-3.1.0+1/lib/ 16 | cached_network_image_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/cached_network_image_platform_interface-1.0.0/lib/ 17 | cached_network_image_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/cached_network_image_web-1.0.1/lib/ 18 | characters:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/characters-1.2.0/lib/ 19 | charcode:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/ 20 | checked_yaml:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/checked_yaml-2.0.1/lib/ 21 | chewie:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/chewie-1.2.2/lib/ 22 | chewie_audio:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/chewie_audio-1.2.0/lib/ 23 | clock:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/clock-1.1.0/lib/ 24 | collection:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/collection-1.15.0/lib/ 25 | convert:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/convert-3.0.2/lib/ 26 | crypto:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.2/lib/ 27 | csslib:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/csslib-0.17.2/lib/ 28 | cupertino_icons:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/cupertino_icons-1.0.5/lib/ 29 | dart_style:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.3/lib/ 30 | dependency_validator:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/dependency_validator-3.2.0/lib/ 31 | device_info_plus:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus-3.2.4/lib/ 32 | device_info_plus_linux:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus_linux-2.1.1/lib/ 33 | device_info_plus_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus_macos-2.2.3/lib/ 34 | device_info_plus_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus_platform_interface-2.3.0+1/lib/ 35 | device_info_plus_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus_web-2.1.0/lib/ 36 | device_info_plus_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus_windows-2.1.1/lib/ 37 | emoji_flag_converter:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/emoji_flag_converter-1.1.0/lib/ 38 | equatable:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/equatable-2.0.3/lib/ 39 | extension:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/extension-0.2.0/lib/ 40 | fake_async:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/fake_async-1.2.0/lib/ 41 | ffi:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/ffi-1.2.1/lib/ 42 | file:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/file-6.1.2/lib/ 43 | fl_chart:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/fl_chart-0.50.6/lib/ 44 | flutter:file:///Users/danieledrisian/flutter/packages/flutter/lib/ 45 | flutter_blurhash:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_blurhash-0.7.0/lib/ 46 | flutter_cache_manager:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_cache_manager-3.3.0/lib/ 47 | flutter_credit_card:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_credit_card-2.0.0/lib/ 48 | flutter_google_places:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_google_places-0.3.0/lib/ 49 | flutter_inappwebview:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_inappwebview-5.4.3+7/lib/ 50 | flutter_layout_grid:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_layout_grid-1.0.6/lib/ 51 | flutter_plugin_android_lifecycle:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-2.0.6/lib/ 52 | flutter_svg:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_svg-0.23.0+1/lib/ 53 | flutter_test:file:///Users/danieledrisian/flutter/packages/flutter_test/lib/ 54 | flutter_web_plugins:file:///Users/danieledrisian/flutter/packages/flutter_web_plugins/lib/ 55 | font_awesome_flutter:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/font_awesome_flutter-10.1.0/lib/ 56 | glob:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2/lib/ 57 | google_api_headers:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/google_api_headers-1.3.0/lib/ 58 | google_fonts:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/google_fonts-2.2.0/lib/ 59 | google_maps_flutter:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/google_maps_flutter-2.1.1/lib/ 60 | google_maps_flutter_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/google_maps_flutter_platform_interface-2.2.0/lib/ 61 | google_maps_webservice:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/google_maps_webservice-0.0.20-nullsafety.5/lib/ 62 | graphs:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/graphs-2.1.0/lib/ 63 | html:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/html-0.15.0/lib/ 64 | http:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/http-0.13.4/lib/ 65 | http_parser:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/http_parser-4.0.1/lib/ 66 | internet_file:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/internet_file-1.0.0+2/lib/ 67 | intl:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/intl-0.17.0/lib/ 68 | io:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/io-1.0.3/lib/ 69 | js:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/js-0.6.3/lib/ 70 | json_annotation:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/json_annotation-4.4.0/lib/ 71 | json_path:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/json_path-0.3.1/lib/ 72 | json_serializable:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/json_serializable-6.1.3/lib/ 73 | logging:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/logging-1.0.2/lib/ 74 | mapbox_search:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/mapbox_search-3.0.1+2/lib/ 75 | matcher:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/matcher-0.12.11/lib/ 76 | material_color_utilities:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/material_color_utilities-0.1.3/lib/ 77 | meta:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/ 78 | mime_type:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/mime_type-1.0.0/lib/ 79 | nested:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/nested-1.0.0/lib/ 80 | numerus:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/numerus-1.1.1/lib/ 81 | octo_image:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/octo_image-1.0.2/lib/ 82 | package_config:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2/lib/ 83 | package_info_plus:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus-1.4.2/lib/ 84 | package_info_plus_linux:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus_linux-1.0.5/lib/ 85 | package_info_plus_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus_macos-1.3.0/lib/ 86 | package_info_plus_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus_platform_interface-1.0.2/lib/ 87 | package_info_plus_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus_web-1.0.5/lib/ 88 | package_info_plus_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus_windows-1.0.5/lib/ 89 | page_transition:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/page_transition-2.0.4/lib/ 90 | path:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path-1.8.0/lib/ 91 | path_drawing:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_drawing-0.5.1+1/lib/ 92 | path_parsing:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_parsing-0.2.1/lib/ 93 | path_provider:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.11/lib/ 94 | path_provider_android:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_android-2.0.14/lib/ 95 | path_provider_ios:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_ios-2.0.9/lib/ 96 | path_provider_linux:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-2.1.7/lib/ 97 | path_provider_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-2.0.6/lib/ 98 | path_provider_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_platform_interface-2.0.4/lib/ 99 | path_provider_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-2.0.7/lib/ 100 | pdfx:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/pdfx-2.0.1+2/lib/ 101 | pedantic:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/pedantic-1.11.1/lib/ 102 | petitparser:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/petitparser-4.4.0/lib/ 103 | photo_view:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/photo_view-0.13.0/lib/ 104 | platform:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/platform-3.1.0/lib/ 105 | plugin_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/plugin_platform_interface-2.1.2/lib/ 106 | pointer_interceptor:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/pointer_interceptor-0.9.3+2/lib/ 107 | process:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/process-4.2.4/lib/ 108 | provider:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/provider-5.0.0/lib/ 109 | pub_semver:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.1/lib/ 110 | pubspec_parse:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/pubspec_parse-1.2.0/lib/ 111 | quiver:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/quiver-3.1.0/lib/ 112 | rfc_6901:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/rfc_6901-0.1.1/lib/ 113 | rive:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/rive-0.7.33/lib/ 114 | rxdart:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/rxdart-0.26.0/lib/ 115 | shared_preferences:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences-2.0.11/lib/ 116 | shared_preferences_android:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_android-2.0.12/lib/ 117 | shared_preferences_ios:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_ios-2.1.1/lib/ 118 | shared_preferences_linux:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_linux-2.1.1/lib/ 119 | shared_preferences_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_macos-2.0.4/lib/ 120 | shared_preferences_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_platform_interface-2.0.0/lib/ 121 | shared_preferences_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_web-2.0.4/lib/ 122 | shared_preferences_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_windows-2.1.1/lib/ 123 | simple_gesture_detector:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/simple_gesture_detector-0.2.0/lib/ 124 | sky_engine:file:///Users/danieledrisian/flutter/bin/cache/pkg/sky_engine/lib/ 125 | source_gen:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/source_gen-1.2.2/lib/ 126 | source_helper:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/source_helper-1.3.2/lib/ 127 | source_span:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.1/lib/ 128 | sqflite:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-2.0.2+1/lib/ 129 | sqflite_common:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite_common-2.2.1+1/lib/ 130 | stack_trace:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.10.0/lib/ 131 | stream_channel:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/stream_channel-2.1.0/lib/ 132 | stream_transform:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/stream_transform-2.0.0/lib/ 133 | string_scanner:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/ 134 | synchronized:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/synchronized-3.0.0+2/lib/ 135 | table_calendar:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/table_calendar-3.0.5/lib/ 136 | term_glyph:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/ 137 | test_api:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/test_api-0.4.8/lib/ 138 | timeago:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/timeago-3.1.0/lib/ 139 | typed_data:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0/lib/ 140 | universal_file:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/universal_file-1.0.0/lib/ 141 | universal_platform:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/universal_platform-1.0.0+1/lib/ 142 | url_launcher:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.15/lib/ 143 | url_launcher_linux:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-2.0.3/lib/ 144 | url_launcher_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-2.0.3/lib/ 145 | url_launcher_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_platform_interface-2.0.5/lib/ 146 | url_launcher_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_web-2.0.11/lib/ 147 | url_launcher_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-2.0.2/lib/ 148 | uuid:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/uuid-3.0.6/lib/ 149 | vector_math:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/vector_math-2.1.1/lib/ 150 | video_player:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-2.2.7/lib/ 151 | video_player_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_platform_interface-4.2.0/lib/ 152 | video_player_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-2.0.10/lib/ 153 | wakelock:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/wakelock-0.5.6/lib/ 154 | wakelock_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/wakelock_macos-0.4.0/lib/ 155 | wakelock_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/wakelock_platform_interface-0.3.0/lib/ 156 | wakelock_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/wakelock_web-0.4.0/lib/ 157 | wakelock_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/wakelock_windows-0.2.0/lib/ 158 | watcher:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1/lib/ 159 | webview_flutter:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/webview_flutter-2.8.0/lib/ 160 | webview_flutter_android:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/webview_flutter_android-2.8.11/lib/ 161 | webview_flutter_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/webview_flutter_platform_interface-1.9.1/lib/ 162 | webview_flutter_wkwebview:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/webview_flutter_wkwebview-2.7.5/lib/ 163 | webviewx:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/webviewx-0.2.1/lib/ 164 | win32:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/win32-2.5.2/lib/ 165 | xdg_directories:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/xdg_directories-0.2.0+1/lib/ 166 | xml:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/xml-5.3.1/lib/ 167 | yaml:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.1/lib/ 168 | youtube_plyr_iframe:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/youtube_plyr_iframe-2.0.7/lib/ 169 | flutterflow_ui:lib/ 170 | --------------------------------------------------------------------------------