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

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

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.dart_tool/
.flutter-plugins-dependencies
.flutter-plugins
pubspec.lock

169
.packages Normal file
View File

@@ -0,0 +1,169 @@
# This file is deprecated. Tools should instead consume
# `.dart_tool/package_config.json`.
#
# For more info see: https://dart.dev/go/dot-packages-deprecation
#
# Generated by pub on 2022-06-14 17:29:42.696281.
_fe_analyzer_shared:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-38.0.0/lib/
analyzer:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/analyzer-3.4.1/lib/
args:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/args-2.3.1/lib/
async:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/async-2.8.2/lib/
auto_size_text:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/auto_size_text-3.0.0/lib/
boolean_selector:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/boolean_selector-2.1.0/lib/
build:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/build-2.3.0/lib/
build_config:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/build_config-1.0.0/lib/
cached_network_image:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/cached_network_image-3.1.0+1/lib/
cached_network_image_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/cached_network_image_platform_interface-1.0.0/lib/
cached_network_image_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/cached_network_image_web-1.0.1/lib/
characters:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/characters-1.2.0/lib/
charcode:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/
checked_yaml:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/checked_yaml-2.0.1/lib/
chewie:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/chewie-1.2.2/lib/
chewie_audio:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/chewie_audio-1.2.0/lib/
clock:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/clock-1.1.0/lib/
collection:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/collection-1.15.0/lib/
convert:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/convert-3.0.2/lib/
crypto:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.2/lib/
csslib:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/csslib-0.17.2/lib/
cupertino_icons:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/cupertino_icons-1.0.5/lib/
dart_style:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.3/lib/
dependency_validator:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/dependency_validator-3.2.0/lib/
device_info_plus:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus-3.2.4/lib/
device_info_plus_linux:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus_linux-2.1.1/lib/
device_info_plus_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus_macos-2.2.3/lib/
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/
device_info_plus_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus_web-2.1.0/lib/
device_info_plus_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/device_info_plus_windows-2.1.1/lib/
emoji_flag_converter:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/emoji_flag_converter-1.1.0/lib/
equatable:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/equatable-2.0.3/lib/
extension:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/extension-0.2.0/lib/
fake_async:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/fake_async-1.2.0/lib/
ffi:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/ffi-1.2.1/lib/
file:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/file-6.1.2/lib/
fl_chart:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/fl_chart-0.50.6/lib/
flutter:file:///Users/danieledrisian/flutter/packages/flutter/lib/
flutter_blurhash:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_blurhash-0.7.0/lib/
flutter_cache_manager:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_cache_manager-3.3.0/lib/
flutter_credit_card:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_credit_card-2.0.0/lib/
flutter_google_places:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_google_places-0.3.0/lib/
flutter_inappwebview:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_inappwebview-5.4.3+7/lib/
flutter_layout_grid:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_layout_grid-1.0.6/lib/
flutter_plugin_android_lifecycle:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-2.0.6/lib/
flutter_svg:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_svg-0.23.0+1/lib/
flutter_test:file:///Users/danieledrisian/flutter/packages/flutter_test/lib/
flutter_web_plugins:file:///Users/danieledrisian/flutter/packages/flutter_web_plugins/lib/
font_awesome_flutter:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/font_awesome_flutter-10.1.0/lib/
glob:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2/lib/
google_api_headers:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/google_api_headers-1.3.0/lib/
google_fonts:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/google_fonts-2.2.0/lib/
google_maps_flutter:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/google_maps_flutter-2.1.1/lib/
google_maps_flutter_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/google_maps_flutter_platform_interface-2.2.0/lib/
google_maps_webservice:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/google_maps_webservice-0.0.20-nullsafety.5/lib/
graphs:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/graphs-2.1.0/lib/
html:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/html-0.15.0/lib/
http:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/http-0.13.4/lib/
http_parser:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/http_parser-4.0.1/lib/
internet_file:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/internet_file-1.0.0+2/lib/
intl:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/intl-0.17.0/lib/
io:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/io-1.0.3/lib/
js:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/js-0.6.3/lib/
json_annotation:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/json_annotation-4.4.0/lib/
json_path:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/json_path-0.3.1/lib/
json_serializable:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/json_serializable-6.1.3/lib/
logging:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/logging-1.0.2/lib/
mapbox_search:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/mapbox_search-3.0.1+2/lib/
matcher:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/matcher-0.12.11/lib/
material_color_utilities:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/material_color_utilities-0.1.3/lib/
meta:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/
mime_type:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/mime_type-1.0.0/lib/
nested:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/nested-1.0.0/lib/
numerus:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/numerus-1.1.1/lib/
octo_image:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/octo_image-1.0.2/lib/
package_config:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2/lib/
package_info_plus:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus-1.4.2/lib/
package_info_plus_linux:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus_linux-1.0.5/lib/
package_info_plus_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus_macos-1.3.0/lib/
package_info_plus_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus_platform_interface-1.0.2/lib/
package_info_plus_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus_web-1.0.5/lib/
package_info_plus_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/package_info_plus_windows-1.0.5/lib/
page_transition:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/page_transition-2.0.4/lib/
path:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path-1.8.0/lib/
path_drawing:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_drawing-0.5.1+1/lib/
path_parsing:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_parsing-0.2.1/lib/
path_provider:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.11/lib/
path_provider_android:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_android-2.0.14/lib/
path_provider_ios:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_ios-2.0.9/lib/
path_provider_linux:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-2.1.7/lib/
path_provider_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-2.0.6/lib/
path_provider_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_platform_interface-2.0.4/lib/
path_provider_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-2.0.7/lib/
pdfx:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/pdfx-2.0.1+2/lib/
pedantic:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/pedantic-1.11.1/lib/
petitparser:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/petitparser-4.4.0/lib/
photo_view:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/photo_view-0.13.0/lib/
platform:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/platform-3.1.0/lib/
plugin_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/plugin_platform_interface-2.1.2/lib/
pointer_interceptor:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/pointer_interceptor-0.9.3+2/lib/
process:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/process-4.2.4/lib/
provider:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/provider-5.0.0/lib/
pub_semver:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.1/lib/
pubspec_parse:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/pubspec_parse-1.2.0/lib/
quiver:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/quiver-3.1.0/lib/
rfc_6901:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/rfc_6901-0.1.1/lib/
rive:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/rive-0.7.33/lib/
rxdart:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/rxdart-0.26.0/lib/
shared_preferences:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences-2.0.11/lib/
shared_preferences_android:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_android-2.0.12/lib/
shared_preferences_ios:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_ios-2.1.1/lib/
shared_preferences_linux:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_linux-2.1.1/lib/
shared_preferences_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_macos-2.0.4/lib/
shared_preferences_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_platform_interface-2.0.0/lib/
shared_preferences_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_web-2.0.4/lib/
shared_preferences_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_windows-2.1.1/lib/
simple_gesture_detector:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/simple_gesture_detector-0.2.0/lib/
sky_engine:file:///Users/danieledrisian/flutter/bin/cache/pkg/sky_engine/lib/
source_gen:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/source_gen-1.2.2/lib/
source_helper:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/source_helper-1.3.2/lib/
source_span:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.1/lib/
sqflite:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-2.0.2+1/lib/
sqflite_common:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite_common-2.2.1+1/lib/
stack_trace:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.10.0/lib/
stream_channel:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/stream_channel-2.1.0/lib/
stream_transform:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/stream_transform-2.0.0/lib/
string_scanner:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/
synchronized:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/synchronized-3.0.0+2/lib/
table_calendar:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/table_calendar-3.0.5/lib/
term_glyph:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/
test_api:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/test_api-0.4.8/lib/
timeago:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/timeago-3.1.0/lib/
typed_data:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0/lib/
universal_file:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/universal_file-1.0.0/lib/
universal_platform:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/universal_platform-1.0.0+1/lib/
url_launcher:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.15/lib/
url_launcher_linux:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-2.0.3/lib/
url_launcher_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-2.0.3/lib/
url_launcher_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_platform_interface-2.0.5/lib/
url_launcher_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_web-2.0.11/lib/
url_launcher_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-2.0.2/lib/
uuid:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/uuid-3.0.6/lib/
vector_math:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/vector_math-2.1.1/lib/
video_player:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-2.2.7/lib/
video_player_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_platform_interface-4.2.0/lib/
video_player_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-2.0.10/lib/
wakelock:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/wakelock-0.5.6/lib/
wakelock_macos:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/wakelock_macos-0.4.0/lib/
wakelock_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/wakelock_platform_interface-0.3.0/lib/
wakelock_web:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/wakelock_web-0.4.0/lib/
wakelock_windows:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/wakelock_windows-0.2.0/lib/
watcher:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1/lib/
webview_flutter:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/webview_flutter-2.8.0/lib/
webview_flutter_android:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/webview_flutter_android-2.8.11/lib/
webview_flutter_platform_interface:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/webview_flutter_platform_interface-1.9.1/lib/
webview_flutter_wkwebview:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/webview_flutter_wkwebview-2.7.5/lib/
webviewx:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/webviewx-0.2.1/lib/
win32:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/win32-2.5.2/lib/
xdg_directories:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/xdg_directories-0.2.0+1/lib/
xml:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/xml-5.3.1/lib/
yaml:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.1/lib/
youtube_plyr_iframe:file:///Users/danieledrisian/flutter/.pub-cache/hosted/pub.dartlang.org/youtube_plyr_iframe-2.0.7/lib/
flutterflow_ui:lib/

26
CHANGELOG.md Normal file
View File

@@ -0,0 +1,26 @@
## 0.3.1
- Remove audio player support from the package
## 0.3.0
- Add support for new UI widgets available in current FlutterFlow release
- Update utility functions to support new FlutterFlow code generation
- Update dependencies
## 0.2.0
**Breaking changes**
- Remove Flutter media plugins such as chewie, video_player, audio_player, google_maps, webview, mapbox, rxdart, shared_preferences
**Features**
- Add support for new UI widgets available in current FlutterFlow release
- Add generated code for animations support
## 0.1.1
Removed FlutterFlowTheme from package
## 0.1.0+4
Initial beta release

29
LICENSE Normal file
View File

@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2024, FlutterFlow
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

101
README.md
View File

@@ -1,4 +1,101 @@
## flutterflow-ui # FlutterFlow UI
flutterflow-ui 本地库 `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.
## Generate code in your FlutterFlow project
In your FlutterFlow project, navigate to the code icon and click on "View Code".
<img src="https://raw.githubusercontent.com/flutterflow/flutterflow-ui/main/assets/package1.gif" width="500" />
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.
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.
After pasting the code, you might encounter some errors, but don't worry. These issues will be resolved through the following steps.
## Add Dependency
Now in your Flutter project, open your `pubspec.yaml` file and add `flutterflow_ui` under dependencies:
```yaml
dependencies:
flutterflow_ui: <latest_version>
```
Remember to run `flutter pub get`
## Replace the `flutter_flow` dependencies with the package import
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.
Remove such imports:
```dart
import '/flutter_flow/flutter_flow_animations.dart';
import '/flutter_flow/flutter_flow_icon_button.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
```
And replace it with the package import:
```dart
import 'package:flutterflow_ui/flutterflow_ui.dart';
```
## Cleaning up unnecessary code
In the beginning of the build method, you might encounter the line `context.watch<FFAppState>();`. 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.
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.
Lastly, double-check that your model file, if it's located in a separate file, is correctly imported.
With these adjustments, you're ready to run the FlutterFlow-generated code in your Flutter project.
__________________________________________
## Some usecases
### How to add a widget with animation to an existing Flutter screen?
* Begin by right-clicking on the component or widget within your FlutterFlow canvas. Then, select "Copy Widget Code."
<img src="https://raw.githubusercontent.com/flutterflow/flutterflow-ui/main/assets/right-click.png" width="500" />
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.
* Next, paste the widget code into your Flutter widget file wherever you'd like to place it.
* 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.
## Supports the following FlutterFlow widgets
* Layout Elements supported by Material/Cupertino package
* Ad Banner
* Audio Player
* Calendar
* Charts
* Checkbox Group
* Choice Chips
* Counter Button
* Credit Card
* Data Table
* Drop Down
* Expandable Image & Circle Image
* Google Map
* Icon Button
* Language Selector
* Media Display
* Mux Broadcast
* Radio Button
* Rive
* Static Map
* Swipeable Stack
* Timer
* Toggle Icon
* Tab Bar
* Web View
## Documentation & more usages
You can check out our [documentation](https://docs.flutterflow.io/flutter/export-flutterflow-ui-code-to-your-flutter-project) for more examples.

115
analysis_options.yaml Normal file
View File

@@ -0,0 +1,115 @@
include: package:flutter_lints/flutter.yaml
analyzer:
errors:
missing_required_param: error
missing_return: warning
todo: ignore
exclude:
- "bin/cache/**"
linter:
rules:
always_declare_return_types: true
always_put_control_body_on_new_line: true
annotate_overrides: true
avoid_bool_literals_in_conditional_expressions: true
avoid_classes_with_only_static_members: true
avoid_empty_else: true
avoid_field_initializers_in_const_classes: true
avoid_function_literals_in_foreach_calls: false
avoid_init_to_null: true
avoid_null_checks_in_equality_operators: true
avoid_relative_lib_imports: true
avoid_renaming_method_parameters: true
avoid_return_types_on_setters: true
avoid_returning_null_for_void: true
avoid_shadowing_type_parameters: true
avoid_slow_async_io: true
avoid_types_as_parameter_names: true
avoid_types_on_closure_parameters: true
avoid_unnecessary_containers: true
avoid_unused_constructor_parameters: true
avoid_void_async: true
await_only_futures: true
camel_case_extensions: true
camel_case_types: true
cancel_subscriptions: true
cascade_invocations: true
close_sinks: true
collection_methods_unrelated_type: true
constant_identifier_names: false
control_flow_in_finally: true
depend_on_referenced_packages: false
directives_ordering: true
empty_catches: true
empty_constructor_bodies: true
empty_statements: true
hash_and_equals: true
implementation_imports: true
library_names: true
library_prefixes: true
library_private_types_in_public_api: true
no_adjacent_strings_in_list: true
no_duplicate_case_values: true
no_leading_underscores_for_local_identifiers: false
overridden_fields: true
package_api_docs: true
package_names: true
package_prefixed_library_names: true
prefer_adjacent_string_concatenation: true
prefer_asserts_in_initializer_lists: true
prefer_collection_literals: true
prefer_conditional_assignment: true
prefer_const_constructors: true
prefer_const_constructors_in_immutables: true
prefer_const_declarations: true
prefer_const_literals_to_create_immutables: true
prefer_contains: true
prefer_final_fields: true
prefer_final_in_for_each: true
prefer_for_elements_to_map_fromIterable: true
prefer_foreach: true
prefer_function_declarations_over_variables: false
prefer_generic_function_type_aliases: true
prefer_if_null_operators: true
prefer_initializing_formals: true
prefer_inlined_adds: true
prefer_interpolation_to_compose_strings: true
prefer_is_empty: true
prefer_is_not_empty: true
prefer_is_not_operator: true
prefer_iterable_whereType: true
prefer_mixin: true
prefer_null_aware_operators: true
prefer_spread_collections: true
prefer_typing_uninitialized_variables: true
prefer_void_to_null: true
recursive_getters: true
slash_for_doc_comments: true
sort_child_properties_last: true
sort_constructors_first: true
sort_pub_dependencies: true
sort_unnamed_constructors_first: true
test_types_in_equals: true
throw_in_finally: true
type_init_formals: true
unawaited_futures: true
unnecessary_await_in_return: true
unnecessary_brace_in_string_interps: true
unnecessary_const: true
unnecessary_getters_setters: true
unnecessary_lambdas: true
unnecessary_new: true
unnecessary_null_aware_assignments: true
unnecessary_null_in_if_null_operators: true
unnecessary_overrides: true
unnecessary_parenthesis: true
unnecessary_statements: true
unnecessary_this: true
unrelated_type_equality_checks: true
use_build_context_synchronously: false
use_full_hex_values_for_flutter_colors: true
use_function_type_syntax_for_parameters: true
use_rethrow_when_possible: true
use_to_and_as_if_applicable: true
valid_regexps: true
void_checks: true

BIN
assets/package1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

BIN
assets/right-click.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

4
lib/flutterflow_ui.dart Normal file
View File

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

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

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

View File

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

View File

@@ -0,0 +1,279 @@
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:from_css_color/from_css_color.dart';
import 'package:intl/intl.dart';
import 'package:json_path/json_path.dart';
import 'package:timeago/timeago.dart' as timeago;
export 'dart:convert' show jsonEncode, jsonDecode;
export 'dart:math' show min, max;
export 'dart:typed_data' show Uint8List;
export 'package:intl/intl.dart';
export 'package:page_transition/page_transition.dart';
export 'flutter_flow_model.dart';
export 'lat_lng.dart';
export 'place.dart';
final RouteObserver<ModalRoute> routeObserver = RouteObserver<PageRoute>();
T valueOrDefault<T>(T? value, T defaultValue) =>
(value is String && value.isEmpty) || value == null ? defaultValue : value;
String dateTimeFormat(String format, DateTime? dateTime, {String? locale}) {
if (dateTime == null) {
return '';
}
if (format == 'relative') {
return timeago.format(dateTime, locale: locale, allowFromNow: true);
}
return DateFormat(format, locale).format(dateTime);
}
Color colorFromCssString(String color, {Color? defaultColor}) {
try {
return fromCssColor(color);
} catch (_) {}
return defaultColor ?? Colors.black;
}
enum FormatType {
decimal,
percent,
scientific,
compact,
compactLong,
custom,
}
enum DecimalType {
automatic,
periodDecimal,
commaDecimal,
}
String formatNumber(
num? value, {
required FormatType formatType,
DecimalType? decimalType,
String? currency,
bool toLowerCase = false,
String? format,
String? locale,
}) {
if (value == null) {
return '';
}
var formattedValue = '';
switch (formatType) {
case FormatType.decimal:
switch (decimalType!) {
case DecimalType.automatic:
formattedValue = NumberFormat.decimalPattern().format(value);
break;
case DecimalType.periodDecimal:
formattedValue = NumberFormat.decimalPattern('en_US').format(value);
break;
case DecimalType.commaDecimal:
formattedValue = NumberFormat.decimalPattern('es_PA').format(value);
break;
}
break;
case FormatType.percent:
formattedValue = NumberFormat.percentPattern().format(value);
break;
case FormatType.scientific:
formattedValue = NumberFormat.scientificPattern().format(value);
if (toLowerCase) {
formattedValue = formattedValue.toLowerCase();
}
break;
case FormatType.compact:
formattedValue = NumberFormat.compact().format(value);
break;
case FormatType.compactLong:
formattedValue = NumberFormat.compactLong().format(value);
break;
case FormatType.custom:
final hasLocale = locale != null && locale.isNotEmpty;
formattedValue =
NumberFormat(format, hasLocale ? locale : null).format(value);
}
if (formattedValue.isEmpty) {
return value.toString();
}
if (currency != null) {
final currencySymbol = currency.isNotEmpty
? currency
: NumberFormat.simpleCurrency().format(0.0).substring(0, 1);
formattedValue = '$currencySymbol$formattedValue';
}
return formattedValue;
}
DateTime get getCurrentTimestamp => DateTime.now();
DateTime dateTimeFromSecondsSinceEpoch(int seconds) {
return DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
}
extension DateTimeConversionExtension on DateTime {
int get secondsSinceEpoch => (millisecondsSinceEpoch / 1000).round();
}
extension DateTimeComparisonOperators on DateTime {
bool operator <(DateTime other) => isBefore(other);
bool operator >(DateTime other) => isAfter(other);
bool operator <=(DateTime other) => this < other || isAtSameMomentAs(other);
bool operator >=(DateTime other) => this > other || isAtSameMomentAs(other);
}
dynamic getJsonField(
dynamic response,
String jsonPath, [
bool isForList = false,
]) {
final field = JsonPath(jsonPath).read(response);
if (field.isEmpty) {
return null;
}
if (field.length > 1) {
return field.map((f) => f.value).toList();
}
final value = field.first.value;
return isForList && value is! Iterable ? [value] : value;
}
Rect? getWidgetBoundingBox(BuildContext context) {
try {
final renderBox = context.findRenderObject() as RenderBox?;
return renderBox!.localToGlobal(Offset.zero) & renderBox.size;
} catch (_) {
return null;
}
}
bool get isAndroid => !kIsWeb && Platform.isAndroid;
bool get isiOS => !kIsWeb && Platform.isIOS;
bool get isWeb => kIsWeb;
const kBreakpointSmall = 479.0;
const kBreakpointMedium = 767.0;
const kBreakpointLarge = 991.0;
bool isMobileWidth(BuildContext context) =>
MediaQuery.sizeOf(context).width < kBreakpointSmall;
bool responsiveVisibility({
required BuildContext context,
bool phone = true,
bool tablet = true,
bool tabletLandscape = true,
bool desktop = true,
}) {
final width = MediaQuery.sizeOf(context).width;
if (width < kBreakpointSmall) {
return phone;
} else if (width < kBreakpointMedium) {
return tablet;
} else if (width < kBreakpointLarge) {
return tabletLandscape;
} else {
return desktop;
}
}
const kTextValidatorUsernameRegex = r'^[a-zA-Z][a-zA-Z0-9_-]{2,16}$';
// https://stackoverflow.com/a/201378
const kTextValidatorEmailRegex =
"^(?:[a-z0-9!#\$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])\$";
const kTextValidatorWebsiteRegex =
r'(https?:\/\/)?(www\.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,10}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)|(https?:\/\/)?(www\.)?(?!ww)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,10}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)';
extension FFTextEditingControllerExt on TextEditingController? {
String get text => this == null ? '' : this!.text;
set text(String newText) => this?.text = newText;
}
extension IterableExt<T> on Iterable<T> {
List<S> mapIndexed<S>(S Function(int, T) func) => toList()
.asMap()
.map((index, value) => MapEntry(index, func(index, value)))
.values
.toList();
}
void showSnackbar(
BuildContext context,
String message, {
bool loading = false,
int duration = 4,
}) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
if (loading)
const Padding(
padding: EdgeInsetsDirectional.only(end: 10.0),
child: SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
),
),
),
Text(message),
],
),
duration: Duration(seconds: duration),
),
);
}
extension FFStringExt on String {
String maybeHandleOverflow({int? maxChars, String replacement = ''}) =>
maxChars != null && length > maxChars
? replaceRange(maxChars, null, replacement)
: this;
}
extension ListFilterExt<T> on Iterable<T?> {
List<T> get withoutNulls => where((s) => s != null).map((e) => e!).toList();
}
extension ListDivideExt<T extends Widget> on Iterable<T> {
Iterable<MapEntry<int, Widget>> get enumerate => toList().asMap().entries;
List<Widget> divide(Widget t) => isEmpty
? []
: (enumerate.map((e) => [e.value, t]).expand((i) => i).toList()
..removeLast());
List<Widget> around(Widget t) => addToStart(t).addToEnd(t);
List<Widget> addToStart(Widget t) =>
enumerate.map((e) => e.value).toList()..insert(0, t);
List<Widget> addToEnd(Widget t) =>
enumerate.map((e) => e.value).toList()..add(t);
List<Padding> paddingTopEach(double val) =>
map((w) => Padding(padding: EdgeInsets.only(top: val), child: w))
.toList();
}
extension StatefulWidgetExtensions on State<StatefulWidget> {
/// Check if the widget exist before safely setting state.
void safeSetState(VoidCallback fn) {
if (mounted) {
// ignore: invalid_use_of_protected_member
setState(fn);
}
}
}

View File

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

View File

@@ -0,0 +1,71 @@
import 'package:flutter/foundation.dart';
import 'package:rive/rive.dart';
class FlutterFlowRiveController extends SimpleAnimation {
FlutterFlowRiveController(
super.animationName, {
super.mix,
super.autoplay,
this.shouldLoop = false,
});
bool shouldLoop;
final _reactivate = ValueNotifier<bool>(false);
ValueListenable<bool> get changeReactivate => _reactivate;
bool get reactivate => _reactivate.value;
set reactivate(bool value) {
if (_reactivate.value != value) {
_reactivate.value = value;
}
}
bool endOfAnimation(LinearAnimationInstance? instance) {
if (instance == null) {
return false;
}
return instance.time == instance.animation.endTime;
}
@override
bool init(RuntimeArtboard artboard) {
reactivate = false;
changeReactivate.addListener(() {
if (reactivate) {
isActive = true;
}
});
return super.init(artboard);
}
@override
void apply(RuntimeArtboard artboard, double elapsedSeconds) {
if (instance == null) {
return;
}
/// Reset on button press
if (reactivate) {
if (endOfAnimation(instance)) {
instance?.time = 0;
}
reactivate = false;
}
if (instance == null || endOfAnimation(instance)) {
isActive = false;
}
/// Stop after one loop if not a continuous animation
if (!shouldLoop &&
(instance?.animation.loop == Loop.loop ||
instance?.animation.loop == Loop.pingPong) &&
instance!.didLoop) {
isActive = false;
}
instance!
..animation.apply(instance!.time, coreContext: artboard, mix: mix)
..advance(elapsedSeconds);
}
}

View File

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

View File

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

View File

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

View File

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

46
lib/src/utils/place.dart Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,305 @@
import 'package:flutter/material.dart';
import 'package:flutter_credit_card/flutter_credit_card.dart';
// ignore: implementation_imports
import 'package:flutter_credit_card/src/masked_text_controller.dart';
export 'package:flutter_credit_card/flutter_credit_card.dart'
show CreditCardModel;
/// Modified from https://pub.dev/packages/flutter_credit_card (see license below)
CreditCardModel emptyCreditCard() => CreditCardModel('', '', '', '', false);
/// A form widget for entering credit card information.
class FlutterFlowCreditCardForm extends StatefulWidget {
/// Creates a [FlutterFlowCreditCardForm].
///
/// - [formKey] is a global key that uniquely identifies the form.
/// - [creditCardModel] is the model that holds the credit card information.
/// - [obscureNumber] determines whether the credit card number should be obscured.
/// - [obscureCvv] determines whether the CVV should be obscured.
/// - [textStyle] is the style of the text in the form.
/// - [spacing] is the spacing between form fields.
/// - [inputDecoration] is the decoration for the form fields.
const FlutterFlowCreditCardForm({
super.key,
required this.formKey,
required this.creditCardModel,
this.obscureNumber = false,
this.obscureCvv = false,
this.textStyle,
this.spacing = 10.0,
this.inputDecoration = const InputDecoration(
border: OutlineInputBorder(),
),
});
final GlobalKey<FormState> formKey;
final CreditCardModel creditCardModel;
final bool obscureNumber;
final bool obscureCvv;
final TextStyle? textStyle;
final double spacing;
final InputDecoration inputDecoration;
@override
State<FlutterFlowCreditCardForm> createState() =>
_FlutterFlowCreditCardFormState();
}
class _FlutterFlowCreditCardFormState extends State<FlutterFlowCreditCardForm> {
final TextEditingController _cardNumberController =
MaskedTextController(mask: '0000 0000 0000 0000');
final TextEditingController _expiryDateController =
MaskedTextController(mask: '00/00');
final TextEditingController _cvvCodeController =
MaskedTextController(mask: '0000');
FocusNode cvvFocusNode = FocusNode();
FocusNode cardNumberNode = FocusNode();
FocusNode expiryDateNode = FocusNode();
String get cardNumber => widget.creditCardModel.cardNumber;
void textFieldFocusDidChange() {
widget.creditCardModel.isCvvFocused = cvvFocusNode.hasFocus;
}
@override
void initState() {
super.initState();
if (widget.creditCardModel.cardNumber.isNotEmpty) {
_cardNumberController.text = widget.creditCardModel.cardNumber;
}
if (widget.creditCardModel.expiryDate.isNotEmpty) {
_expiryDateController.text = widget.creditCardModel.expiryDate;
}
if (widget.creditCardModel.cvvCode.isNotEmpty) {
_cvvCodeController.text = widget.creditCardModel.cvvCode;
}
cvvFocusNode.addListener(textFieldFocusDidChange);
_cardNumberController.addListener(() => setState(
() => widget.creditCardModel.cardNumber = _cardNumberController.text));
_expiryDateController.addListener(() => setState(
() => widget.creditCardModel.expiryDate = _expiryDateController.text));
_cvvCodeController.addListener(() => setState(
() => widget.creditCardModel.cvvCode = _cvvCodeController.text));
}
@override
void dispose() {
cvvFocusNode.dispose();
expiryDateNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Form(
key: widget.formKey,
child: Column(
children: <Widget>[
Container(
margin: const EdgeInsets.only(top: 12.0),
child: TextFormField(
obscureText: widget.obscureNumber,
controller: _cardNumberController,
onEditingComplete: () =>
FocusScope.of(context).requestFocus(expiryDateNode),
style: widget.textStyle,
decoration: widget.inputDecoration.copyWith(
labelText: 'Card number',
hintText: 'XXXX XXXX XXXX XXXX',
labelStyle: widget.textStyle,
hintStyle: widget.textStyle,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
// Validate less that 13 digits +3 white spaces
if (value == null || value.isEmpty || value.length < 16) {
return 'Please input a valid number';
}
return null;
},
),
),
SizedBox(height: widget.spacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: Container(
margin: const EdgeInsets.only(top: 8.0, bottom: 8.0),
child: TextFormField(
controller: _expiryDateController,
focusNode: expiryDateNode,
onEditingComplete: () {
FocusScope.of(context).requestFocus(cvvFocusNode);
},
style: widget.textStyle,
decoration: widget.inputDecoration.copyWith(
labelText: 'Exp. Date',
hintText: 'MM/YY',
labelStyle: widget.textStyle,
hintStyle: widget.textStyle,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please input a valid date';
}
final DateTime now = DateTime.now();
final List<String> date = value.split(RegExp(r'/'));
final int month = int.parse(date.first);
final int year = int.parse('20\${date.last}');
final DateTime cardDate = DateTime(year, month);
if (cardDate.isBefore(now) ||
month > 12 ||
month == 0) {
return 'Please input a valid date';
}
return null;
},
),
),
),
const SizedBox(width: 16.0),
Expanded(
child: Container(
margin: const EdgeInsets.only(top: 8.0, bottom: 8.0),
child: TextFormField(
obscureText: widget.obscureCvv,
focusNode: cvvFocusNode,
controller: _cvvCodeController,
style: widget.textStyle,
decoration: widget.inputDecoration.copyWith(
labelText: 'CVV',
hintText: 'XXX',
labelStyle: widget.textStyle,
hintStyle: widget.textStyle,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null ||
value.isEmpty ||
value.length < 3) {
return 'Please input a valid CVV';
}
return null;
},
),
),
),
],
),
],
),
);
/// Credit Card prefix patterns as of March 2019
/// A [List<String>] represents a range.
/// i.e. ['51', '55'] represents the range of cards starting with '51' to those starting with '55'
Map<CardType, Set<List<String>>> cardNumPatterns =
<CardType, Set<List<String>>>{
CardType.visa: <List<String>>{
<String>['4'],
},
CardType.americanExpress: <List<String>>{
<String>['34'],
<String>['37'],
},
CardType.discover: <List<String>>{
<String>['6011'],
<String>['622126', '622925'],
<String>['644', '649'],
<String>['65']
},
CardType.mastercard: <List<String>>{
<String>['51', '55'],
<String>['2221', '2229'],
<String>['223', '229'],
<String>['23', '26'],
<String>['270', '271'],
<String>['2720'],
},
};
/// This function determines the Credit Card type based on the cardPatterns
/// and returns it.
CardType detectCCType(String cardNumber) {
//Default card type is other
CardType cardType = CardType.otherBrand;
if (cardNumber.isEmpty) {
return cardType;
}
cardNumPatterns.forEach(
(type, patterns) {
for (final patternRange in patterns) {
// Remove any spaces
String ccPatternStr = cardNumber.replaceAll(RegExp(r's+\b|\bs'), '');
final int rangeLen = patternRange[0].length;
// Trim the Credit Card number string to match the pattern prefix length
if (rangeLen < cardNumber.length) {
ccPatternStr = ccPatternStr.substring(0, rangeLen);
}
if (patternRange.length > 1) {
// Convert the prefix range into numbers then make sure the
// Credit Card num is in the pattern range.
// Because Strings don't have '>=' type operators
final int ccPrefixAsInt = int.parse(ccPatternStr);
final int startPatternPrefixAsInt = int.parse(patternRange[0]);
final int endPatternPrefixAsInt = int.parse(patternRange[1]);
if (ccPrefixAsInt >= startPatternPrefixAsInt &&
ccPrefixAsInt <= endPatternPrefixAsInt) {
// Found a match
cardType = type;
break;
}
} else {
// Just compare the single pattern prefix with the Credit Card prefix
if (ccPatternStr == patternRange[0]) {
// Found a match
cardType = type;
break;
}
}
}
},
);
return cardType;
}
}
/// BSD 2-Clause License
/// Copyright (c) 2019, Simform Solutions
/// All rights reserved.
/// Redistribution and use in source and binary forms, with or without
/// modification, are permitted provided that the following conditions are met:
/// 1. Redistributions of source code must retain the above copyright notice, this
/// list of conditions and the following disclaimer.
/// 2. Redistributions in binary form must reproduce the above copyright notice,
/// this list of conditions and the following disclaimer in the documentation
/// and/or other materials provided with the distribution.
/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
/// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
/// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
/// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
/// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
/// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
/// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
/// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
/// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
/// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,185 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
/// A customizable icon button widget.
class FlutterFlowIconButton extends StatefulWidget {
/// Creates a [FlutterFlowIconButton].
///
/// - [icon] parameter is required and specifies the widget to be used as the icon.
/// - [borderRadius] parameter specifies the border radius of the button.
/// - [buttonSize] parameter specifies the size of the button.
/// - [fillColor] parameter specifies the fill color of the button.
/// - [disabledColor] parameter specifies the color of the button when it is disabled.
/// - [disabledIconColor] parameter specifies the color of the icon when the button is disabled.
/// - [hoverColor] parameter specifies the color of the button when it is hovered.
/// - [hoverIconColor] parameter specifies the color of the icon when the button is hovered.
/// - [borderColor] parameter specifies the border color of the button.
/// - [borderWidth] parameter specifies the width of the button's border.
/// - [showLoadingIndicator] parameter specifies whether to show a loading indicator on the button.
/// - [onPressed] parameter specifies the callback function to be called when the button is pressed.
const FlutterFlowIconButton({
super.key,
required this.icon,
this.borderColor,
this.borderRadius,
this.borderWidth,
this.buttonSize,
this.fillColor,
this.disabledColor,
this.disabledIconColor,
this.hoverColor,
this.hoverIconColor,
this.onPressed,
this.showLoadingIndicator = false,
});
final Widget icon;
final double? borderRadius;
final double? buttonSize;
final Color? fillColor;
final Color? disabledColor;
final Color? disabledIconColor;
final Color? hoverColor;
final Color? hoverIconColor;
final Color? borderColor;
final double? borderWidth;
final bool showLoadingIndicator;
final Function()? onPressed;
@override
State<FlutterFlowIconButton> createState() => _FlutterFlowIconButtonState();
}
class _FlutterFlowIconButtonState extends State<FlutterFlowIconButton> {
bool loading = false;
late double? iconSize;
late Color? iconColor;
late Widget effectiveIcon;
@override
void initState() {
super.initState();
_updateIcon();
}
@override
void didUpdateWidget(FlutterFlowIconButton oldWidget) {
super.didUpdateWidget(oldWidget);
_updateIcon();
}
void _updateIcon() {
final isFontAwesome = widget.icon is FaIcon;
if (isFontAwesome) {
FaIcon icon = widget.icon as FaIcon;
effectiveIcon = FaIcon(
icon.icon,
size: icon.size,
);
iconSize = icon.size;
iconColor = icon.color;
} else {
Icon icon = widget.icon as Icon;
effectiveIcon = Icon(
icon.icon,
size: icon.size,
);
iconSize = icon.size;
iconColor = icon.color;
}
}
@override
Widget build(BuildContext context) {
ButtonStyle style = ButtonStyle(
shape: WidgetStateProperty.resolveWith<OutlinedBorder>(
(states) {
return RoundedRectangleBorder(
borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
side: BorderSide(
color: widget.borderColor ?? Colors.transparent,
width: widget.borderWidth ?? 0,
),
);
},
),
iconColor: WidgetStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(WidgetState.disabled) &&
widget.disabledIconColor != null) {
return widget.disabledIconColor;
}
if (states.contains(WidgetState.hovered) &&
widget.hoverIconColor != null) {
return widget.hoverIconColor;
}
return iconColor;
},
),
backgroundColor: WidgetStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(WidgetState.disabled) &&
widget.disabledColor != null) {
return widget.disabledColor;
}
if (states.contains(WidgetState.hovered) &&
widget.hoverColor != null) {
return widget.hoverColor;
}
return widget.fillColor;
},
),
overlayColor: WidgetStateProperty.resolveWith<Color?>((states) {
if (states.contains(WidgetState.pressed)) {
return null;
}
return widget.hoverColor == null ? null : Colors.transparent;
}),
);
return SizedBox(
width: widget.buttonSize,
height: widget.buttonSize,
child: Theme(
data: ThemeData.from(
colorScheme: Theme.of(context).colorScheme,
useMaterial3: true,
),
child: IgnorePointer(
ignoring: widget.showLoadingIndicator && loading,
child: IconButton(
icon: (widget.showLoadingIndicator && loading)
? SizedBox(
width: iconSize,
height: iconSize,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
iconColor ?? Colors.white,
),
),
)
: effectiveIcon,
onPressed: widget.onPressed == null
? null
: () async {
if (loading) {
return;
}
setState(() => loading = true);
try {
await widget.onPressed!();
} finally {
if (mounted) {
setState(() => loading = false);
}
}
},
splashRadius: widget.buttonSize,
style: style,
),
),
),
);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,318 @@
/*
* Copyright 2020 https://github.com/TercyoStorck
*
* Source code has been modified by FlutterFlow, Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import 'package:flutter/material.dart';
import 'package:flutterflow_ui/src/utils/form_field_controller.dart';
/// A custom radio button widget that allows the user to select a single option from a list of options.
class FlutterFlowRadioButton extends StatefulWidget {
/// Creates a [FlutterFlowRadioButton].
const FlutterFlowRadioButton({
super.key,
required this.options,
required this.onChanged,
required this.controller,
required this.optionHeight,
required this.textStyle,
this.optionWidth,
this.selectedTextStyle,
this.textPadding = EdgeInsets.zero,
this.buttonPosition = RadioButtonPosition.left,
this.direction = Axis.vertical,
required this.radioButtonColor,
this.inactiveRadioButtonColor,
this.toggleable = false,
this.horizontalAlignment = WrapAlignment.start,
this.verticalAlignment = WrapCrossAlignment.start,
});
/// The list of options to choose from.
final List<String> options;
/// A callback function that will be called when the selected option changes.
final Function(String?)? onChanged;
/// A form field controller that manages the state of the selected option.
final FormFieldController<String> controller;
/// The height of each option.
final double optionHeight;
/// The width of each option. If not provided, the width will be determined automatically.
final double? optionWidth;
/// The style of the option text.
final TextStyle textStyle;
/// The style of the selected option text. If not provided, the [textStyle] will be used.
final TextStyle? selectedTextStyle;
/// The padding around the option text.
final EdgeInsetsGeometry textPadding;
/// The position of the radio button relative to the option text.
final RadioButtonPosition buttonPosition;
/// The direction in which the options are laid out.
final Axis direction;
/// The color of the radio button.
final Color radioButtonColor;
/// The color of the radio button when it is not selected. If not provided, the [radioButtonColor] will be used.
final Color? inactiveRadioButtonColor;
/// Whether the radio button can be toggled on and off.
final bool toggleable;
/// The horizontal alignment of the options when the direction is horizontal.
final WrapAlignment horizontalAlignment;
/// The vertical alignment of the options when the direction is vertical.
final WrapCrossAlignment verticalAlignment;
@override
State<FlutterFlowRadioButton> createState() => _FlutterFlowRadioButtonState();
}
class _FlutterFlowRadioButtonState extends State<FlutterFlowRadioButton> {
bool get enabled => widget.onChanged != null;
FormFieldController<String> get controller => widget.controller;
void Function()? _listener;
@override
void initState() {
super.initState();
_maybeSetOnChangedListener();
}
@override
void dispose() {
_maybeRemoveOnChangedListener();
super.dispose();
}
@override
void didUpdateWidget(FlutterFlowRadioButton oldWidget) {
super.didUpdateWidget(oldWidget);
final oldWidgetEnabled = oldWidget.onChanged != null;
if (oldWidgetEnabled != enabled) {
_maybeRemoveOnChangedListener();
_maybeSetOnChangedListener();
}
}
void _maybeSetOnChangedListener() {
if (enabled) {
_listener = () => widget.onChanged!(controller.value);
controller.addListener(_listener!);
}
}
void _maybeRemoveOnChangedListener() {
if (_listener != null) {
controller.removeListener(_listener!);
_listener = null;
}
}
List<String> get effectiveOptions =>
widget.options.isEmpty ? ['[Option]'] : widget.options;
@override
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context)
.copyWith(unselectedWidgetColor: widget.inactiveRadioButtonColor),
child: RadioGroup<String>.builder(
direction: widget.direction,
groupValue: controller.value,
onChanged: enabled ? (value) => controller.value = value : null,
activeColor: widget.radioButtonColor,
toggleable: widget.toggleable,
textStyle: widget.textStyle,
selectedTextStyle: widget.selectedTextStyle ?? widget.textStyle,
textPadding: widget.textPadding,
optionHeight: widget.optionHeight,
optionWidth: widget.optionWidth,
horizontalAlignment: widget.horizontalAlignment,
verticalAlignment: widget.verticalAlignment,
items: effectiveOptions,
itemBuilder: (item) =>
RadioButtonBuilder(item, buttonPosition: widget.buttonPosition),
),
);
}
}
enum RadioButtonPosition {
right,
left,
}
class RadioButtonBuilder<T> {
RadioButtonBuilder(
this.description, {
this.buttonPosition = RadioButtonPosition.left,
});
final String description;
final RadioButtonPosition buttonPosition;
}
class RadioButton<T> extends StatelessWidget {
const RadioButton({
super.key,
required this.description,
required this.value,
required this.groupValue,
required this.onChanged,
required this.buttonPosition,
required this.activeColor,
required this.toggleable,
required this.textStyle,
required this.selectedTextStyle,
required this.textPadding,
this.shouldFlex = false,
});
final String description;
final T value;
final T? groupValue;
final void Function(T?)? onChanged;
final RadioButtonPosition buttonPosition;
final Color activeColor;
final bool toggleable;
final TextStyle textStyle;
final TextStyle selectedTextStyle;
final EdgeInsetsGeometry textPadding;
final bool shouldFlex;
@override
Widget build(BuildContext context) {
final selectedStyle = selectedTextStyle;
final isSelected = value == groupValue;
Widget radioButtonText = Padding(
padding: textPadding,
child: Text(
description,
style: isSelected ? selectedStyle : textStyle,
),
);
if (shouldFlex) {
radioButtonText = Flexible(child: radioButtonText);
}
return InkWell(
onTap: onChanged != null ? () => onChanged!(value) : null,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (buttonPosition == RadioButtonPosition.right) radioButtonText,
Radio<T>(
groupValue: groupValue,
onChanged: onChanged,
value: value,
activeColor: activeColor,
toggleable: toggleable,
),
if (buttonPosition == RadioButtonPosition.left) radioButtonText,
],
),
);
}
}
class RadioGroup<T> extends StatelessWidget {
const RadioGroup.builder({
super.key,
required this.groupValue,
required this.onChanged,
required this.items,
required this.itemBuilder,
required this.direction,
required this.optionHeight,
required this.horizontalAlignment,
required this.activeColor,
required this.toggleable,
required this.textStyle,
required this.selectedTextStyle,
required this.textPadding,
this.optionWidth,
this.verticalAlignment = WrapCrossAlignment.center,
});
final T? groupValue;
final List<T> items;
final RadioButtonBuilder Function(T value) itemBuilder;
final void Function(T?)? onChanged;
final Axis direction;
final double optionHeight;
final double? optionWidth;
final WrapAlignment horizontalAlignment;
final WrapCrossAlignment verticalAlignment;
final Color activeColor;
final bool toggleable;
final TextStyle textStyle;
final TextStyle selectedTextStyle;
final EdgeInsetsGeometry textPadding;
List<Widget> get _group => items.map(
(item) {
final radioButtonBuilder = itemBuilder(item);
return SizedBox(
height: optionHeight,
width: optionWidth,
child: RadioButton(
description: radioButtonBuilder.description,
value: item,
groupValue: groupValue,
onChanged: onChanged,
buttonPosition: radioButtonBuilder.buttonPosition,
activeColor: activeColor,
toggleable: toggleable,
textStyle: textStyle,
selectedTextStyle: selectedTextStyle,
textPadding: textPadding,
shouldFlex: optionWidth != null,
),
);
},
).toList();
@override
Widget build(BuildContext context) => direction == Axis.horizontal
? Wrap(
direction: direction,
alignment: horizontalAlignment,
children: _group,
)
: Wrap(
direction: direction,
crossAxisAlignment: verticalAlignment,
children: _group,
);
}

View File

@@ -0,0 +1,146 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutterflow_ui/src/utils/lat_lng.dart';
import 'package:mapbox_search/mapbox_search.dart' as mapbox;
/// A widget that displays a static map using the Mapbox API.
class FlutterFlowStaticMap extends StatelessWidget {
const FlutterFlowStaticMap({
super.key,
required this.location,
required this.apiKey,
required this.style,
required this.width,
required this.height,
this.fit,
this.borderRadius = BorderRadius.zero,
this.markerColor,
this.markerUrl,
this.cached = false,
this.zoom = 12,
this.tilt = 0,
this.rotation = 0,
});
/// The location to display on the map.
final LatLng location;
/// The API key for accessing the Mapbox API.
final String apiKey;
/// The style of the map.
final mapbox.MapBoxStyle style;
/// The width of the map widget.
final double width;
/// The height of the map widget.
final double height;
/// How the map should be inscribed into the available space.
final BoxFit? fit;
/// The border radius of the map widget.
final BorderRadius borderRadius;
/// The color of the marker on the map.
final Color? markerColor;
/// The URL of the custom marker icon.
final String? markerUrl;
/// Whether to cache the map image.
final bool cached;
/// The zoom level of the map.
final int zoom;
/// The tilt angle of the map camera.
final int tilt;
/// The rotation angle of the map camera.
final int rotation;
@override
Widget build(BuildContext context) {
final imageWidth = width.clamp(1, 1280).toInt();
final imageHeight = height.clamp(1, 1280).toInt();
final imagePath = getStaticMapImageURL(location, apiKey, style, imageWidth,
imageHeight, markerColor, markerUrl, zoom, rotation, tilt);
return ClipRRect(
borderRadius: borderRadius,
child: cached
? CachedNetworkImage(
imageUrl: imagePath,
width: width,
height: height,
fit: fit,
)
: Image.network(
imagePath,
width: width,
height: height,
fit: fit,
),
);
}
}
String getStaticMapImageURL(
LatLng location,
String apiKey,
mapbox.MapBoxStyle mapStyle,
int width,
int height,
Color? markerColor,
String? markerURL,
int zoom,
int rotation,
int tilt,
) {
final finalLocation = (
lat: location.latitude.clamp(-90, 90).toDouble(),
long: location.longitude.clamp(-180, 180).toDouble(),
);
final finalRotation = rotation.clamp(-180, 180).round();
final finalTilt = tilt.clamp(0, 60).round();
final finalZoom = zoom.clamp(0, 22).round();
final image = mapbox.StaticImage(apiKey: apiKey);
if (markerColor == null && (markerURL == null || markerURL.trim().isEmpty)) {
return image
.getStaticUrlWithoutMarker(
center: finalLocation,
style: mapStyle,
width: width.round(),
height: height.round(),
zoomLevel: finalZoom,
bearing: finalRotation,
pitch: finalTilt,
)
.toString();
} else {
return image
.getStaticUrlWithMarker(
marker: markerURL == null || markerURL.trim().isEmpty
? mapbox.MapBoxMarker(
markerColor: mapbox.RgbColor(
markerColor!.red,
markerColor.green,
markerColor.blue,
),
markerLetter: mapbox.MakiIcons.circle.value,
markerSize: mapbox.MarkerSize.MEDIUM,
)
: null,
markerUrl: markerURL,
center: finalLocation,
style: mapStyle,
width: width.round(),
height: height.round(),
zoomLevel: finalZoom,
bearing: finalRotation,
pitch: finalTilt,
)
.toString();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

83
pubspec.yaml Normal file
View File

@@ -0,0 +1,83 @@
name: flutterflow_ui
description: Flutter package that makes it easy to use UI widgets and code generated by FlutterFlow in your Flutter projects.
homepage: https://docs.flutterflow.io
repository: https://github.com/FlutterFlow/flutterflow-ui
version: 0.3.1
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
aligned_dialog: ^0.0.6
aligned_tooltip: ^0.0.1
apivideo_live_stream: 1.0.7
auto_size_text: ^3.0.0
barcode_widget: ^2.0.3
cached_network_image: ^3.3.1
carousel_slider: ^4.2.1
collection: ^1.18.0
data_table_2: ^2.5.10
dropdown_button2: ^2.3.9
easy_debounce: ^2.0.1
emoji_flag_converter: ^1.1.0
equatable: ^2.0.5
expandable: ^5.0.1
file_picker: ^8.0.3
fl_chart: ^0.68.0
flip_card: ^0.7.0
floating_bottom_navigation_bar: ^1.5.2
flutter:
sdk: flutter
flutter_animate: ^4.5.0
flutter_card_swiper: ^6.0.0
flutter_credit_card: ^4.0.1
flutter_plugin_android_lifecycle: ^2.0.20
flutter_staggered_grid_view: ^0.7.0
font_awesome_flutter: ^10.6.0
from_css_color: ^2.0.0
google_fonts: ^6.1.0
# google_maps: ^7.1.0
# google_maps_flutter: ^2.6.1
# google_maps_flutter_android: ^2.8.1
# google_maps_flutter_ios: ^2.6.1
# google_maps_flutter_platform_interface: ^2.7.1
# google_maps_flutter_web: ^0.5.7
# google_mobile_ads: ^5.1.0
http: ^1.2.1
intl: ^0.19.0
json_path: ^0.7.2
mapbox_search: ^4.2.2
mime_type: ^1.0.0
native_device_orientation: ^1.2.1
page_transition: ^2.1.0
percent_indicator: ^4.2.2
photo_view: ^0.14.0
plugin_platform_interface: ^2.1.8
pointer_interceptor: ^0.10.1
provider: ^6.1.2
rive: ^0.12.2
shared_preferences: ^2.2.2
shared_preferences_android: ^2.2.1
shared_preferences_foundation: ^2.3.4
shared_preferences_platform_interface: ^2.3.1
shared_preferences_web: ^2.2.1
smooth_page_indicator: ^1.1.0
stop_watch_timer: ^3.0.2
substring_highlight: ^1.0.33
table_calendar: ^3.1.1
timeago: ^3.6.1
url_launcher: ^6.2.5
url_launcher_android: ^6.3.0
url_launcher_ios: ^6.2.5
url_launcher_platform_interface: ^2.3.2
webview_flutter: ^4.7.0
webview_flutter_android: ^3.15.0
webview_flutter_platform_interface: ^2.10.0
webview_flutter_wkwebview: ^3.12.0
webviewx_plus: ^0.5.0
dev_dependencies:
flutter_lints: ^4.0.0
flutter:
uses-material-design: true