基于flutterflow-ui v0.3.1版本 去除google ad 与google map
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.flutter-plugins
|
||||
pubspec.lock
|
||||
169
.packages
Normal file
169
.packages
Normal 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
26
CHANGELOG.md
Normal 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
29
LICENSE
Normal 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
101
README.md
@@ -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
115
analysis_options.yaml
Normal 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
BIN
assets/package1.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 MiB |
BIN
assets/right-click.png
Normal file
BIN
assets/right-click.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
4
lib/flutterflow_ui.dart
Normal file
4
lib/flutterflow_ui.dart
Normal 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
155
lib/src/constants.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Constants that we should use throughout the app for widget dimension/stylings.
|
||||
|
||||
/// Dimensions
|
||||
|
||||
const double kPadding2px = 2.0;
|
||||
const double kPadding4px = 4.0;
|
||||
const double kPadding8px = 8.0;
|
||||
const double kPadding12px = 12.0;
|
||||
const double kPadding16px = 16.0;
|
||||
const double kPadding20px = 20.0;
|
||||
const double kPadding24px = 24.0;
|
||||
const double kPadding28px = 28.0;
|
||||
|
||||
const double kBaseSize8px = 8.0;
|
||||
const double kBaseSize12px = 12.0;
|
||||
const double kBaseSize16px = 16.0;
|
||||
const double kBaseSize20px = 20.0;
|
||||
const double kBaseSize24px = 24.0;
|
||||
const double kBaseSize28px = 28.0;
|
||||
const double kBaseSize32px = 32.0;
|
||||
const double kBaseSize36px = 36.0;
|
||||
const double kBaseSize48px = 48.0;
|
||||
const double kBaseSize60px = 60.0;
|
||||
const double kBaseSize72px = 72.0;
|
||||
const double kBaseSize96px = 96.0;
|
||||
const double kBaseSize120px = 120.0;
|
||||
const double kBaseSize160px = 160.0;
|
||||
|
||||
const double kWidth48px = 48.0;
|
||||
const double kWidth56px = 56.0;
|
||||
//* Width for a property editor that is a quarter of the width of panel.
|
||||
const double kWidth60px = 60.0;
|
||||
const double kWidth64px = 64.0;
|
||||
const double kWidth72px = 72.0;
|
||||
const double kWidth80px = 80.0;
|
||||
const double kWidth96px = 96.0;
|
||||
const double kWidth108px = 108.0;
|
||||
const double kWidth120px = 120.0;
|
||||
//* Width for a property editor that is half the width of panel.
|
||||
const double kWidth132px = 132.0;
|
||||
const double kWidth144px = 144.0;
|
||||
const double kWidth160px = 160.0;
|
||||
//* Width for a property editor that is total the width of panel.
|
||||
const double kWidth276px = 276.0;
|
||||
|
||||
const double kWidth440px = 440.0;
|
||||
const double kWidth480px = 480.0;
|
||||
const double kWidth600px = 600.0;
|
||||
const double kWidth720px = 720.0;
|
||||
const double kWidth840px = 840.0;
|
||||
|
||||
const double kHeight16px = 16.0;
|
||||
const double kHeight20px = 20.0;
|
||||
const double kHeight24px = 24.0;
|
||||
const double kHeight28px = 28.0;
|
||||
//* Typically used for input fields' height.
|
||||
const double kHeight32px = 32.0;
|
||||
const double kHeight36px = 36.0;
|
||||
const double kHeight40px = 40.0;
|
||||
const double kHeight48px = 48.0;
|
||||
const double kHeight56px = 56.0;
|
||||
const double kHeight64px = 64.0;
|
||||
const double kHeight72px = 72.0;
|
||||
const double kHeight80px = 80.0;
|
||||
const double kHeight88px = 88.0;
|
||||
const double kHeight96px = 96.0;
|
||||
|
||||
const double kHeight240px = 240.0;
|
||||
const double kHeight320px = 320.0;
|
||||
const double kHeight400px = 400.0;
|
||||
const double kHeight480px = 480.0;
|
||||
|
||||
/// Border Radius
|
||||
|
||||
const double kBorderRadius4px = 4.0;
|
||||
const double kBorderRadius8px = 8.0;
|
||||
const double kBorderRadius12px = 12.0;
|
||||
|
||||
/// Border Width
|
||||
|
||||
const double kBorderWidth1px = 1.0;
|
||||
const double kBorderWidth1_5px = 1.5;
|
||||
const double kBorderWidth2px = 2.0;
|
||||
|
||||
/// Line Width
|
||||
|
||||
const double kLineWidth1px = 1.0;
|
||||
const double kLineWidth2px = 2.0;
|
||||
const double kLineWidth4px = 4.0;
|
||||
|
||||
//* Typically used for icon size in property name line.
|
||||
const double kIconSize14px = 14.0;
|
||||
const double kIconSize16px = 16.0;
|
||||
const double kIconSize20px = 20.0;
|
||||
const double kIconSize22px = 22.0;
|
||||
const double kIconSize24px = 24.0;
|
||||
const double kIconSize32px = 32.0;
|
||||
|
||||
/// Durations
|
||||
|
||||
const Duration kDuration250ms = Duration(milliseconds: 250);
|
||||
const Duration kDuration500ms = Duration(milliseconds: 500);
|
||||
|
||||
/// Durations
|
||||
|
||||
const Curve kCurveEase = Curves.ease;
|
||||
|
||||
/// Elevations and shadowing
|
||||
|
||||
const double kElevation0 = 0.0;
|
||||
const double kElevation2 = 2.0;
|
||||
const double kElevation4 = 4.0;
|
||||
const double kElevation8 = 8.0;
|
||||
|
||||
final kBoxShadow2 = kElevationToShadow[2]!;
|
||||
final kBoxShadow4 = kElevationToShadow[4]!;
|
||||
final kBoxShadow8 = kElevationToShadow[8]!;
|
||||
|
||||
/// Blur
|
||||
|
||||
const double kBlur4 = 4.0;
|
||||
const double kBlur8 = 8.0;
|
||||
|
||||
/// Opacity
|
||||
|
||||
const double kOpacity0_2 = 0.2;
|
||||
const double kOpacity0_4 = 0.4;
|
||||
const double kOpacity0_6 = 0.6;
|
||||
const double kOpacity0_8 = 0.8;
|
||||
|
||||
/// Color
|
||||
|
||||
const kErrorColor = Color(0xFFDF3F3F);
|
||||
const kPrimaryColor = Color(0xFF4B39EF);
|
||||
const kSecondaryColor = Color(0xFFEE8B60);
|
||||
const kTertiaryColor = Color(0xFF1D2429);
|
||||
const kSuccessToastColor = Color(0xFF39D2C0);
|
||||
const kGrey250 = Color(0xFFE3E7ED);
|
||||
const kGrey800 = Color(0xFF424E5A);
|
||||
const kDiffAddedColor = Colors.green;
|
||||
const kDiffDeletedColor = Color.fromARGB(255, 255, 129, 129);
|
||||
|
||||
/// Font Sizes
|
||||
|
||||
const double kFontSize12px = 12.0;
|
||||
const double kFontSize13px = 13.0;
|
||||
const double kFontSize14px = 14.0;
|
||||
const double kFontSize16px = 16.0;
|
||||
const double kFontSize18px = 18.0;
|
||||
const double kFontSize20px = 20.0;
|
||||
const double kFontSize24px = 24.0;
|
||||
const double kFontSize32px = 32.0;
|
||||
const double kFontSize36px = 36.0;
|
||||
103
lib/src/utils/flutter_flow_animations.dart
Normal file
103
lib/src/utils/flutter_flow_animations.dart
Normal file
@@ -0,0 +1,103 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
enum AnimationTrigger {
|
||||
onPageLoad,
|
||||
onActionTrigger,
|
||||
}
|
||||
|
||||
class AnimationInfo {
|
||||
AnimationInfo({
|
||||
required this.trigger,
|
||||
required this.effectsBuilder,
|
||||
this.loop = false,
|
||||
this.reverse = false,
|
||||
this.applyInitialState = true,
|
||||
});
|
||||
|
||||
final AnimationTrigger trigger;
|
||||
final List<Effect> Function()? effectsBuilder;
|
||||
final bool applyInitialState;
|
||||
final bool loop;
|
||||
final bool reverse;
|
||||
late AnimationController controller;
|
||||
|
||||
List<Effect>? _effects;
|
||||
|
||||
List<Effect> get effects => _effects ??= effectsBuilder!();
|
||||
|
||||
void maybeUpdateEffects(List<Effect>? updatedEffects) {
|
||||
if (updatedEffects != null) {
|
||||
_effects = updatedEffects;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void createAnimation(AnimationInfo animation, TickerProvider vsync) {
|
||||
final newController = AnimationController(vsync: vsync);
|
||||
animation.controller = newController;
|
||||
}
|
||||
|
||||
void setupAnimations(Iterable<AnimationInfo> animations, TickerProvider vsync) {
|
||||
animations.forEach((animation) => createAnimation(animation, vsync));
|
||||
}
|
||||
|
||||
extension AnimatedWidgetExtension on Widget {
|
||||
Widget animateOnPageLoad(
|
||||
AnimationInfo animationInfo, {
|
||||
List<Effect>? effects,
|
||||
}) {
|
||||
animationInfo.maybeUpdateEffects(effects);
|
||||
return Animate(
|
||||
effects: animationInfo.effects,
|
||||
child: this,
|
||||
onPlay: (controller) => animationInfo.loop ? controller.repeat(reverse: animationInfo.reverse) : null,
|
||||
onComplete: (controller) => !animationInfo.loop && animationInfo.reverse ? controller.reverse() : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget animateOnActionTrigger(
|
||||
AnimationInfo animationInfo, {
|
||||
List<Effect>? effects,
|
||||
bool hasBeenTriggered = false,
|
||||
}) {
|
||||
animationInfo.maybeUpdateEffects(effects);
|
||||
return hasBeenTriggered || animationInfo.applyInitialState
|
||||
? Animate(controller: animationInfo.controller, autoPlay: false, effects: animationInfo.effects, child: this)
|
||||
: this;
|
||||
}
|
||||
}
|
||||
|
||||
class TiltEffect extends Effect<Offset> {
|
||||
const TiltEffect({
|
||||
super.delay,
|
||||
super.duration,
|
||||
super.curve,
|
||||
Offset? begin,
|
||||
Offset? end,
|
||||
}) : super(
|
||||
begin: begin ?? const Offset(0.0, 0.0),
|
||||
end: end ?? const Offset(0.0, 0.0),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
AnimationController controller,
|
||||
EffectEntry entry,
|
||||
) {
|
||||
Animation<Offset> animation = buildAnimation(controller, entry);
|
||||
return getOptimizedBuilder<Offset>(
|
||||
animation: animation,
|
||||
builder: (_, __) => Transform(
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001)
|
||||
..rotateX(animation.value.dx)
|
||||
..rotateY(animation.value.dy),
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
279
lib/src/utils/flutter_flow_helpers.dart
Normal file
279
lib/src/utils/flutter_flow_helpers.dart
Normal file
@@ -0,0 +1,279 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:from_css_color/from_css_color.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:json_path/json_path.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
export 'dart:convert' show jsonEncode, jsonDecode;
|
||||
export 'dart:math' show min, max;
|
||||
export 'dart:typed_data' show Uint8List;
|
||||
|
||||
export 'package:intl/intl.dart';
|
||||
export 'package:page_transition/page_transition.dart';
|
||||
|
||||
export 'flutter_flow_model.dart';
|
||||
export 'lat_lng.dart';
|
||||
export 'place.dart';
|
||||
|
||||
final RouteObserver<ModalRoute> routeObserver = RouteObserver<PageRoute>();
|
||||
|
||||
T valueOrDefault<T>(T? value, T defaultValue) =>
|
||||
(value is String && value.isEmpty) || value == null ? defaultValue : value;
|
||||
|
||||
String dateTimeFormat(String format, DateTime? dateTime, {String? locale}) {
|
||||
if (dateTime == null) {
|
||||
return '';
|
||||
}
|
||||
if (format == 'relative') {
|
||||
return timeago.format(dateTime, locale: locale, allowFromNow: true);
|
||||
}
|
||||
return DateFormat(format, locale).format(dateTime);
|
||||
}
|
||||
|
||||
Color colorFromCssString(String color, {Color? defaultColor}) {
|
||||
try {
|
||||
return fromCssColor(color);
|
||||
} catch (_) {}
|
||||
return defaultColor ?? Colors.black;
|
||||
}
|
||||
|
||||
enum FormatType {
|
||||
decimal,
|
||||
percent,
|
||||
scientific,
|
||||
compact,
|
||||
compactLong,
|
||||
custom,
|
||||
}
|
||||
|
||||
enum DecimalType {
|
||||
automatic,
|
||||
periodDecimal,
|
||||
commaDecimal,
|
||||
}
|
||||
|
||||
String formatNumber(
|
||||
num? value, {
|
||||
required FormatType formatType,
|
||||
DecimalType? decimalType,
|
||||
String? currency,
|
||||
bool toLowerCase = false,
|
||||
String? format,
|
||||
String? locale,
|
||||
}) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
var formattedValue = '';
|
||||
switch (formatType) {
|
||||
case FormatType.decimal:
|
||||
switch (decimalType!) {
|
||||
case DecimalType.automatic:
|
||||
formattedValue = NumberFormat.decimalPattern().format(value);
|
||||
break;
|
||||
case DecimalType.periodDecimal:
|
||||
formattedValue = NumberFormat.decimalPattern('en_US').format(value);
|
||||
break;
|
||||
case DecimalType.commaDecimal:
|
||||
formattedValue = NumberFormat.decimalPattern('es_PA').format(value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case FormatType.percent:
|
||||
formattedValue = NumberFormat.percentPattern().format(value);
|
||||
break;
|
||||
case FormatType.scientific:
|
||||
formattedValue = NumberFormat.scientificPattern().format(value);
|
||||
if (toLowerCase) {
|
||||
formattedValue = formattedValue.toLowerCase();
|
||||
}
|
||||
break;
|
||||
case FormatType.compact:
|
||||
formattedValue = NumberFormat.compact().format(value);
|
||||
break;
|
||||
case FormatType.compactLong:
|
||||
formattedValue = NumberFormat.compactLong().format(value);
|
||||
break;
|
||||
case FormatType.custom:
|
||||
final hasLocale = locale != null && locale.isNotEmpty;
|
||||
formattedValue =
|
||||
NumberFormat(format, hasLocale ? locale : null).format(value);
|
||||
}
|
||||
|
||||
if (formattedValue.isEmpty) {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
if (currency != null) {
|
||||
final currencySymbol = currency.isNotEmpty
|
||||
? currency
|
||||
: NumberFormat.simpleCurrency().format(0.0).substring(0, 1);
|
||||
formattedValue = '$currencySymbol$formattedValue';
|
||||
}
|
||||
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
DateTime get getCurrentTimestamp => DateTime.now();
|
||||
DateTime dateTimeFromSecondsSinceEpoch(int seconds) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
|
||||
}
|
||||
|
||||
extension DateTimeConversionExtension on DateTime {
|
||||
int get secondsSinceEpoch => (millisecondsSinceEpoch / 1000).round();
|
||||
}
|
||||
|
||||
extension DateTimeComparisonOperators on DateTime {
|
||||
bool operator <(DateTime other) => isBefore(other);
|
||||
bool operator >(DateTime other) => isAfter(other);
|
||||
bool operator <=(DateTime other) => this < other || isAtSameMomentAs(other);
|
||||
bool operator >=(DateTime other) => this > other || isAtSameMomentAs(other);
|
||||
}
|
||||
|
||||
dynamic getJsonField(
|
||||
dynamic response,
|
||||
String jsonPath, [
|
||||
bool isForList = false,
|
||||
]) {
|
||||
final field = JsonPath(jsonPath).read(response);
|
||||
if (field.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (field.length > 1) {
|
||||
return field.map((f) => f.value).toList();
|
||||
}
|
||||
final value = field.first.value;
|
||||
return isForList && value is! Iterable ? [value] : value;
|
||||
}
|
||||
|
||||
Rect? getWidgetBoundingBox(BuildContext context) {
|
||||
try {
|
||||
final renderBox = context.findRenderObject() as RenderBox?;
|
||||
return renderBox!.localToGlobal(Offset.zero) & renderBox.size;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isAndroid => !kIsWeb && Platform.isAndroid;
|
||||
bool get isiOS => !kIsWeb && Platform.isIOS;
|
||||
bool get isWeb => kIsWeb;
|
||||
|
||||
const kBreakpointSmall = 479.0;
|
||||
const kBreakpointMedium = 767.0;
|
||||
const kBreakpointLarge = 991.0;
|
||||
bool isMobileWidth(BuildContext context) =>
|
||||
MediaQuery.sizeOf(context).width < kBreakpointSmall;
|
||||
bool responsiveVisibility({
|
||||
required BuildContext context,
|
||||
bool phone = true,
|
||||
bool tablet = true,
|
||||
bool tabletLandscape = true,
|
||||
bool desktop = true,
|
||||
}) {
|
||||
final width = MediaQuery.sizeOf(context).width;
|
||||
if (width < kBreakpointSmall) {
|
||||
return phone;
|
||||
} else if (width < kBreakpointMedium) {
|
||||
return tablet;
|
||||
} else if (width < kBreakpointLarge) {
|
||||
return tabletLandscape;
|
||||
} else {
|
||||
return desktop;
|
||||
}
|
||||
}
|
||||
|
||||
const kTextValidatorUsernameRegex = r'^[a-zA-Z][a-zA-Z0-9_-]{2,16}$';
|
||||
// https://stackoverflow.com/a/201378
|
||||
const kTextValidatorEmailRegex =
|
||||
"^(?:[a-z0-9!#\$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])\$";
|
||||
const kTextValidatorWebsiteRegex =
|
||||
r'(https?:\/\/)?(www\.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,10}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)|(https?:\/\/)?(www\.)?(?!ww)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,10}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)';
|
||||
|
||||
extension FFTextEditingControllerExt on TextEditingController? {
|
||||
String get text => this == null ? '' : this!.text;
|
||||
set text(String newText) => this?.text = newText;
|
||||
}
|
||||
|
||||
extension IterableExt<T> on Iterable<T> {
|
||||
List<S> mapIndexed<S>(S Function(int, T) func) => toList()
|
||||
.asMap()
|
||||
.map((index, value) => MapEntry(index, func(index, value)))
|
||||
.values
|
||||
.toList();
|
||||
}
|
||||
|
||||
void showSnackbar(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
bool loading = false,
|
||||
int duration = 4,
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
if (loading)
|
||||
const Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 10.0),
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(message),
|
||||
],
|
||||
),
|
||||
duration: Duration(seconds: duration),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
extension FFStringExt on String {
|
||||
String maybeHandleOverflow({int? maxChars, String replacement = ''}) =>
|
||||
maxChars != null && length > maxChars
|
||||
? replaceRange(maxChars, null, replacement)
|
||||
: this;
|
||||
}
|
||||
|
||||
extension ListFilterExt<T> on Iterable<T?> {
|
||||
List<T> get withoutNulls => where((s) => s != null).map((e) => e!).toList();
|
||||
}
|
||||
|
||||
extension ListDivideExt<T extends Widget> on Iterable<T> {
|
||||
Iterable<MapEntry<int, Widget>> get enumerate => toList().asMap().entries;
|
||||
|
||||
List<Widget> divide(Widget t) => isEmpty
|
||||
? []
|
||||
: (enumerate.map((e) => [e.value, t]).expand((i) => i).toList()
|
||||
..removeLast());
|
||||
|
||||
List<Widget> around(Widget t) => addToStart(t).addToEnd(t);
|
||||
|
||||
List<Widget> addToStart(Widget t) =>
|
||||
enumerate.map((e) => e.value).toList()..insert(0, t);
|
||||
|
||||
List<Widget> addToEnd(Widget t) =>
|
||||
enumerate.map((e) => e.value).toList()..add(t);
|
||||
|
||||
List<Padding> paddingTopEach(double val) =>
|
||||
map((w) => Padding(padding: EdgeInsets.only(top: val), child: w))
|
||||
.toList();
|
||||
}
|
||||
|
||||
extension StatefulWidgetExtensions on State<StatefulWidget> {
|
||||
/// Check if the widget exist before safely setting state.
|
||||
void safeSetState(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
// ignore: invalid_use_of_protected_member
|
||||
setState(fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
lib/src/utils/flutter_flow_model.dart
Normal file
172
lib/src/utils/flutter_flow_model.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
Widget wrapWithModel<T extends FlutterFlowModel>({
|
||||
required T model,
|
||||
required Widget child,
|
||||
required VoidCallback updateCallback,
|
||||
bool updateOnChange = false,
|
||||
}) {
|
||||
// Set the component to optionally update the page on updates.
|
||||
model
|
||||
..setOnUpdate(
|
||||
onUpdate: updateCallback,
|
||||
updateOnChange: updateOnChange,
|
||||
)
|
||||
// Models for components within a page will be disposed by the page's model,
|
||||
// so we don't want the component widget to dispose them until the page is
|
||||
// itself disposed.
|
||||
..disposeOnWidgetDisposal = false;
|
||||
// Wrap in a Provider so that the model can be accessed by the component.
|
||||
return Provider<T>.value(
|
||||
value: model,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
T createModel<T extends FlutterFlowModel>(
|
||||
BuildContext context,
|
||||
T Function() defaultBuilder,
|
||||
) {
|
||||
final model = context.read<T?>() ?? defaultBuilder()
|
||||
.._init(context);
|
||||
return model;
|
||||
}
|
||||
|
||||
abstract class FlutterFlowModel<W extends Widget> {
|
||||
// Initialization methods
|
||||
bool _isInitialized = false;
|
||||
void initState(BuildContext context);
|
||||
void _init(BuildContext context) {
|
||||
if (!_isInitialized) {
|
||||
initState(context);
|
||||
_isInitialized = true;
|
||||
}
|
||||
if (context.widget is W) {
|
||||
_widget = context.widget as W;
|
||||
}
|
||||
}
|
||||
|
||||
// The widget associated with this model. This is useful for accessing the
|
||||
// parameters of the widget, for example.
|
||||
W? _widget;
|
||||
// This will always be non-null when used, but is nullable to allow us to
|
||||
// dispose of the widget in the [dispose] method (for garbage collection).
|
||||
W get widget => _widget!;
|
||||
|
||||
// Dispose methods
|
||||
// Whether to dispose this model when the corresponding widget is
|
||||
// disposed. By default this is true for pages and false for components,
|
||||
// as page/component models handle the disposal of their children.
|
||||
bool disposeOnWidgetDisposal = true;
|
||||
void dispose();
|
||||
void maybeDispose() {
|
||||
if (disposeOnWidgetDisposal) {
|
||||
dispose();
|
||||
}
|
||||
// Remove reference to widget for garbage collection purposes.
|
||||
_widget = null;
|
||||
}
|
||||
|
||||
// Whether to update the containing page / component on updates.
|
||||
bool updateOnChange = false;
|
||||
// Function to call when the model receives an update.
|
||||
VoidCallback _updateCallback = () {};
|
||||
void onUpdate() => updateOnChange ? _updateCallback() : () {};
|
||||
FlutterFlowModel setOnUpdate({
|
||||
bool updateOnChange = false,
|
||||
required VoidCallback onUpdate,
|
||||
}) =>
|
||||
this
|
||||
.._updateCallback = onUpdate
|
||||
..updateOnChange = updateOnChange;
|
||||
// Update the containing page when this model received an update.
|
||||
void updatePage(VoidCallback callback) {
|
||||
callback();
|
||||
_updateCallback();
|
||||
}
|
||||
}
|
||||
|
||||
class FlutterFlowDynamicModels<T extends FlutterFlowModel> {
|
||||
FlutterFlowDynamicModels(this.defaultBuilder);
|
||||
|
||||
final T Function() defaultBuilder;
|
||||
final Map<String, T> _childrenModels = {};
|
||||
final Map<String, int> _childrenIndexes = {};
|
||||
Set<String>? _activeKeys;
|
||||
|
||||
T getModel(String uniqueKey, int index) {
|
||||
_updateActiveKeys(uniqueKey);
|
||||
_childrenIndexes[uniqueKey] = index;
|
||||
return _childrenModels[uniqueKey] ??= defaultBuilder();
|
||||
}
|
||||
|
||||
List<S> getValues<S>(S? Function(T) getValue) {
|
||||
return _childrenIndexes.entries
|
||||
// Sort keys by index.
|
||||
.sorted((a, b) => a.value.compareTo(b.value))
|
||||
.where((e) => _childrenModels[e.key] != null)
|
||||
// Map each model to the desired value and return as list. In order
|
||||
// to preserve index order, rather than removing null values we provide
|
||||
// default values (for types with reasonable defaults).
|
||||
.map((e) => getValue(_childrenModels[e.key]!) ?? _getDefaultValue<S>()!)
|
||||
.toList();
|
||||
}
|
||||
|
||||
S? getValueAtIndex<S>(int index, S? Function(T) getValue) {
|
||||
final uniqueKey =
|
||||
_childrenIndexes.entries.firstWhereOrNull((e) => e.value == index)?.key;
|
||||
return getValueForKey(uniqueKey, getValue);
|
||||
}
|
||||
|
||||
S? getValueForKey<S>(String? uniqueKey, S? Function(T) getValue) {
|
||||
final model = _childrenModels[uniqueKey];
|
||||
return model != null ? getValue(model) : null;
|
||||
}
|
||||
|
||||
void dispose() => _childrenModels.values.forEach((model) => model.dispose());
|
||||
|
||||
void _updateActiveKeys(String uniqueKey) {
|
||||
final shouldResetActiveKeys = _activeKeys == null;
|
||||
_activeKeys ??= {};
|
||||
_activeKeys!.add(uniqueKey);
|
||||
|
||||
if (shouldResetActiveKeys) {
|
||||
// Add a post-frame callback to remove and dispose of unused models after
|
||||
// we're done building, then reset `_activeKeys` to null so we know to do
|
||||
// this again next build.
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
_childrenIndexes.removeWhere((k, _) => !_activeKeys!.contains(k));
|
||||
_childrenModels.keys
|
||||
.toSet()
|
||||
.difference(_activeKeys!)
|
||||
// Remove and dispose of unused models since they are not being used
|
||||
// elsewhere and would not otherwise be disposed.
|
||||
.forEach((k) => _childrenModels.remove(k)?.maybeDispose());
|
||||
_activeKeys = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
T? _getDefaultValue<T>() {
|
||||
switch (T) {
|
||||
case const (int):
|
||||
return 0 as T;
|
||||
case const (double):
|
||||
return 0.0 as T;
|
||||
case const (String):
|
||||
return '' as T;
|
||||
case const (bool):
|
||||
return false as T;
|
||||
default:
|
||||
return null as T;
|
||||
}
|
||||
}
|
||||
|
||||
extension TextValidationExtensions on String? Function(BuildContext, String?)? {
|
||||
String? Function(String?)? asValidator(BuildContext context) =>
|
||||
this != null ? (val) => this!(context, val) : null;
|
||||
}
|
||||
71
lib/src/utils/flutter_flow_rive_controller.dart
Normal file
71
lib/src/utils/flutter_flow_rive_controller.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:rive/rive.dart';
|
||||
|
||||
class FlutterFlowRiveController extends SimpleAnimation {
|
||||
FlutterFlowRiveController(
|
||||
super.animationName, {
|
||||
super.mix,
|
||||
super.autoplay,
|
||||
this.shouldLoop = false,
|
||||
});
|
||||
|
||||
bool shouldLoop;
|
||||
final _reactivate = ValueNotifier<bool>(false);
|
||||
ValueListenable<bool> get changeReactivate => _reactivate;
|
||||
|
||||
bool get reactivate => _reactivate.value;
|
||||
set reactivate(bool value) {
|
||||
if (_reactivate.value != value) {
|
||||
_reactivate.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
bool endOfAnimation(LinearAnimationInstance? instance) {
|
||||
if (instance == null) {
|
||||
return false;
|
||||
}
|
||||
return instance.time == instance.animation.endTime;
|
||||
}
|
||||
|
||||
@override
|
||||
bool init(RuntimeArtboard artboard) {
|
||||
reactivate = false;
|
||||
changeReactivate.addListener(() {
|
||||
if (reactivate) {
|
||||
isActive = true;
|
||||
}
|
||||
});
|
||||
return super.init(artboard);
|
||||
}
|
||||
|
||||
@override
|
||||
void apply(RuntimeArtboard artboard, double elapsedSeconds) {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/// Reset on button press
|
||||
if (reactivate) {
|
||||
if (endOfAnimation(instance)) {
|
||||
instance?.time = 0;
|
||||
}
|
||||
reactivate = false;
|
||||
}
|
||||
|
||||
if (instance == null || endOfAnimation(instance)) {
|
||||
isActive = false;
|
||||
}
|
||||
|
||||
/// Stop after one loop if not a continuous animation
|
||||
if (!shouldLoop &&
|
||||
(instance?.animation.loop == Loop.loop ||
|
||||
instance?.animation.loop == Loop.pingPong) &&
|
||||
instance!.didLoop) {
|
||||
isActive = false;
|
||||
}
|
||||
|
||||
instance!
|
||||
..animation.apply(instance!.time, coreContext: artboard, mix: mix)
|
||||
..advance(elapsedSeconds);
|
||||
}
|
||||
}
|
||||
11
lib/src/utils/flutter_flow_utils.dart
Normal file
11
lib/src/utils/flutter_flow_utils.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
library flutterflow_ui;
|
||||
|
||||
export 'flutter_flow_animations.dart';
|
||||
export 'flutter_flow_helpers.dart';
|
||||
export 'flutter_flow_rive_controller.dart';
|
||||
export 'form_field_controller.dart';
|
||||
export 'internationalization.dart';
|
||||
export 'lat_lng.dart';
|
||||
export 'place.dart';
|
||||
export 'random_data.dart';
|
||||
export 'uploaded_file.dart';
|
||||
24
lib/src/utils/form_field_controller.dart
Normal file
24
lib/src/utils/form_field_controller.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class FormFieldController<T> extends ValueNotifier<T?> {
|
||||
FormFieldController(this.initialValue) : super(initialValue);
|
||||
|
||||
final T? initialValue;
|
||||
|
||||
void reset() => value = initialValue;
|
||||
|
||||
void update() => notifyListeners();
|
||||
}
|
||||
|
||||
// If the initial value is a list (which it is for multiselect),
|
||||
// we need to use this controller to avoid a pass by reference issue
|
||||
// that can result in the initial value being modified.
|
||||
class FormListFieldController<T> extends FormFieldController<List<T>> {
|
||||
FormListFieldController(super.initialValue)
|
||||
: _initialListValue = List<T>.from(initialValue ?? []);
|
||||
|
||||
final List<T>? _initialListValue;
|
||||
|
||||
@override
|
||||
void reset() => value = List<T>.from(_initialListValue ?? []);
|
||||
}
|
||||
47
lib/src/utils/internationalization.dart
Normal file
47
lib/src/utils/internationalization.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
//Helper class for internationalization
|
||||
class FFLocalizations {
|
||||
FFLocalizations(this.locale);
|
||||
|
||||
final Locale locale;
|
||||
|
||||
//Initializing
|
||||
static FFLocalizations of(BuildContext context) =>
|
||||
FFLocalizations(const Locale('en'));
|
||||
|
||||
static List<String> languages() => ['en'];
|
||||
|
||||
String get languageCode => locale.languageCode;
|
||||
|
||||
int get languageIndex => languages().contains(languageCode)
|
||||
? languages().indexOf(languageCode)
|
||||
: 0;
|
||||
|
||||
String getText(String key) =>
|
||||
(kTranslationsMap[key] ?? {})[locale.languageCode] ?? '';
|
||||
|
||||
String getVariableText({
|
||||
String? enText = '',
|
||||
}) =>
|
||||
[enText][languageIndex] ?? '';
|
||||
}
|
||||
|
||||
class FFLocalizationsDelegate extends LocalizationsDelegate<FFLocalizations> {
|
||||
const FFLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
FFLocalizations.languages().contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
Future<FFLocalizations> load(Locale locale) =>
|
||||
SynchronousFuture<FFLocalizations>(FFLocalizations(locale));
|
||||
|
||||
@override
|
||||
bool shouldReload(FFLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
final kTranslationsMap =
|
||||
<Map<String, Map<String, String>>>[].reduce((a, b) => a..addAll(b));
|
||||
19
lib/src/utils/lat_lng.dart
Normal file
19
lib/src/utils/lat_lng.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
class LatLng {
|
||||
const LatLng(this.latitude, this.longitude);
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
|
||||
@override
|
||||
String toString() => 'LatLng(lat: $latitude, lng: $longitude)';
|
||||
|
||||
String serialize() => '$latitude,$longitude';
|
||||
|
||||
@override
|
||||
int get hashCode => latitude.hashCode + longitude.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is LatLng &&
|
||||
latitude == other.latitude &&
|
||||
longitude == other.longitude;
|
||||
}
|
||||
46
lib/src/utils/place.dart
Normal file
46
lib/src/utils/place.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'lat_lng.dart';
|
||||
|
||||
class FFPlace {
|
||||
const FFPlace({
|
||||
this.latLng = const LatLng(0.0, 0.0),
|
||||
this.name = '',
|
||||
this.address = '',
|
||||
this.city = '',
|
||||
this.state = '',
|
||||
this.country = '',
|
||||
this.zipCode = '',
|
||||
});
|
||||
|
||||
final LatLng latLng;
|
||||
final String name;
|
||||
final String address;
|
||||
final String city;
|
||||
final String state;
|
||||
final String country;
|
||||
final String zipCode;
|
||||
|
||||
@override
|
||||
String toString() => '''FFPlace(
|
||||
latLng: $latLng,
|
||||
name: $name,
|
||||
address: $address,
|
||||
city: $city,
|
||||
state: $state,
|
||||
country: $country,
|
||||
zipCode: $zipCode,
|
||||
)''';
|
||||
|
||||
@override
|
||||
int get hashCode => latLng.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is FFPlace &&
|
||||
latLng == other.latLng &&
|
||||
name == other.name &&
|
||||
address == other.address &&
|
||||
city == other.city &&
|
||||
state == other.state &&
|
||||
country == other.country &&
|
||||
zipCode == other.zipCode;
|
||||
}
|
||||
51
lib/src/utils/random_data.dart
Normal file
51
lib/src/utils/random_data.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final _random = Random();
|
||||
|
||||
int randomInteger(int min, int max) {
|
||||
return _random.nextInt(max - min + 1) + min;
|
||||
}
|
||||
|
||||
double randomDouble(double min, double max) {
|
||||
return _random.nextDouble() * (max - min) + min;
|
||||
}
|
||||
|
||||
String randomString(
|
||||
int minLength,
|
||||
int maxLength,
|
||||
bool lowercaseAz,
|
||||
bool uppercaseAz,
|
||||
bool digits,
|
||||
) {
|
||||
var chars = '';
|
||||
if (lowercaseAz) {
|
||||
chars += 'abcdefghijklmnopqrstuvwxyz';
|
||||
}
|
||||
if (uppercaseAz) {
|
||||
chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
}
|
||||
if (digits) {
|
||||
chars += '0123456789';
|
||||
}
|
||||
return List.generate(randomInteger(minLength, maxLength),
|
||||
(index) => chars[_random.nextInt(chars.length)]).join();
|
||||
}
|
||||
|
||||
// Random date between 1970 and 2025.
|
||||
DateTime randomDate() {
|
||||
// Random max must be in range 0 < max <= 2^32.
|
||||
// So we have to generate the time in seconds and then convert to milliseconds.
|
||||
return DateTime.fromMillisecondsSinceEpoch(
|
||||
randomInteger(0, 1735689600) * 1000);
|
||||
}
|
||||
|
||||
String randomImageUrl(int width, int height) {
|
||||
return 'https://picsum.photos/seed/${_random.nextInt(1000)}/$width/$height';
|
||||
}
|
||||
|
||||
Color randomColor() {
|
||||
return Color.fromARGB(
|
||||
255, _random.nextInt(255), _random.nextInt(255), _random.nextInt(255));
|
||||
}
|
||||
68
lib/src/utils/uploaded_file.dart
Normal file
68
lib/src/utils/uploaded_file.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data' show Uint8List;
|
||||
|
||||
class FFUploadedFile {
|
||||
const FFUploadedFile({
|
||||
this.name,
|
||||
this.bytes,
|
||||
this.height,
|
||||
this.width,
|
||||
this.blurHash,
|
||||
});
|
||||
|
||||
final String? name;
|
||||
final Uint8List? bytes;
|
||||
final double? height;
|
||||
final double? width;
|
||||
final String? blurHash;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'FFUploadedFile(name: $name, bytes: ${bytes?.length ?? 0}, height: $height, width: $width, blurHash: $blurHash,)';
|
||||
|
||||
String serialize() => jsonEncode(
|
||||
{
|
||||
'name': name,
|
||||
'bytes': bytes,
|
||||
'height': height,
|
||||
'width': width,
|
||||
'blurHash': blurHash,
|
||||
},
|
||||
);
|
||||
|
||||
static FFUploadedFile deserialize(String val) {
|
||||
final serializedData = jsonDecode(val) as Map<String, dynamic>;
|
||||
final data = {
|
||||
'name': serializedData['name'] ?? '',
|
||||
'bytes': serializedData['bytes'] ?? Uint8List.fromList([]),
|
||||
'height': serializedData['height'],
|
||||
'width': serializedData['width'],
|
||||
'blurHash': serializedData['blurHash'],
|
||||
};
|
||||
return FFUploadedFile(
|
||||
name: data['name'] as String,
|
||||
bytes: Uint8List.fromList(data['bytes'].cast<int>().toList()),
|
||||
height: data['height'] as double?,
|
||||
width: data['width'] as double?,
|
||||
blurHash: data['blurHash'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
name,
|
||||
bytes,
|
||||
height,
|
||||
width,
|
||||
blurHash,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is FFUploadedFile &&
|
||||
name == other.name &&
|
||||
bytes == other.bytes &&
|
||||
height == other.height &&
|
||||
width == other.width &&
|
||||
blurHash == other.blurHash;
|
||||
}
|
||||
148
lib/src/widgets/flutter_flow_ad_banner.dart
Normal file
148
lib/src/widgets/flutter_flow_ad_banner.dart
Normal file
@@ -0,0 +1,148 @@
|
||||
// import 'dart:io';
|
||||
|
||||
// import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/scheduler.dart';
|
||||
// import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||
|
||||
// /// A widget that displays a banner ad.
|
||||
// class FlutterFlowAdBanner extends StatefulWidget {
|
||||
// const FlutterFlowAdBanner({
|
||||
// super.key,
|
||||
// this.width,
|
||||
// this.height,
|
||||
// required this.showsTestAd,
|
||||
// this.iOSAdUnitID,
|
||||
// this.androidAdUnitID,
|
||||
// });
|
||||
|
||||
// /// The width of the ad banner.
|
||||
// final double? width;
|
||||
|
||||
// /// The height of the ad banner.
|
||||
// final double? height;
|
||||
|
||||
// /// Whether to show a test ad.
|
||||
// final bool showsTestAd;
|
||||
|
||||
// /// The Ad Unit ID for iOS.
|
||||
// final String? iOSAdUnitID;
|
||||
|
||||
// /// The Ad Unit ID for Android.
|
||||
// final String? androidAdUnitID;
|
||||
|
||||
// @override
|
||||
// State<FlutterFlowAdBanner> createState() => _FlutterFlowAdBannerState();
|
||||
// }
|
||||
|
||||
// class _FlutterFlowAdBannerState extends State<FlutterFlowAdBanner> {
|
||||
// static const AdRequest request = AdRequest();
|
||||
|
||||
// BannerAd? _anchoredBanner;
|
||||
// AdWidget? adWidget;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
|
||||
// SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
// _createAnchoredBanner(context);
|
||||
// });
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void dispose() {
|
||||
// super.dispose();
|
||||
// _anchoredBanner?.dispose();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// var loadingText = 'Ad Loading... \\n\\n';
|
||||
// if (widget.showsTestAd) {
|
||||
// loadingText +=
|
||||
// 'If this takes a long time, you may have to check whether the ad is '
|
||||
// 'being covered from a parent widget. For example, a larger width than '
|
||||
// 'the device screen size or a large border radius encompassing the ad banner '
|
||||
// 'may stop ads from loading.\\n\\n'
|
||||
// 'If a full-width banner is desired for your app, leave the width and '
|
||||
// 'height of the AdBanner widget empty. AdBanner will automatically'
|
||||
// 'match the size of the banner to the device screen.';
|
||||
// }
|
||||
|
||||
// return _anchoredBanner != null && adWidget != null
|
||||
// ? Container(
|
||||
// alignment: Alignment.center,
|
||||
// color: Colors.red,
|
||||
// width: _anchoredBanner!.size.width.toDouble(),
|
||||
// height: _anchoredBanner!.size.height.toDouble(),
|
||||
// child: adWidget,
|
||||
// )
|
||||
// : Container(
|
||||
// color: Colors.black,
|
||||
// alignment: Alignment.center,
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: Text(
|
||||
// loadingText,
|
||||
// style: const TextStyle(
|
||||
// fontSize: 10.0,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// /// Creates an anchored banner ad.
|
||||
// Future _createAnchoredBanner(BuildContext context) async {
|
||||
// final AdSize? size = widget.width != null && widget.height != null
|
||||
// ? AdSize(
|
||||
// height: widget.height!.toInt(),
|
||||
// width: widget.width!.toInt(),
|
||||
// )
|
||||
// : await AdSize.getAnchoredAdaptiveBannerAdSize(
|
||||
// widget.width == null ? Orientation.portrait : Orientation.landscape,
|
||||
// widget.width == null
|
||||
// ? MediaQuery.sizeOf(context).width.truncate()
|
||||
// : MediaQuery.sizeOf(context).height.truncate(),
|
||||
// );
|
||||
|
||||
// if (size == null) {
|
||||
// print('Unable to get size of anchored banner.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// final isAndroid = !kIsWeb && Platform.isAndroid;
|
||||
// final BannerAd banner = BannerAd(
|
||||
// size: size,
|
||||
// request: request,
|
||||
// adUnitId: widget.showsTestAd
|
||||
// ? isAndroid
|
||||
// ? 'ca-app-pub-3940256099942544/6300978111'
|
||||
// : 'ca-app-pub-3940256099942544/2934735716'
|
||||
// : isAndroid
|
||||
// ? widget.androidAdUnitID!
|
||||
// : widget.iOSAdUnitID!,
|
||||
// listener: BannerAdListener(
|
||||
// onAdLoaded: (ad) {
|
||||
// print('\$BannerAd loaded.');
|
||||
// if (mounted) {
|
||||
// setState(() => _anchoredBanner = ad as BannerAd);
|
||||
// }
|
||||
// },
|
||||
// onAdFailedToLoad: (ad, error) {
|
||||
// print('\$BannerAd failedToLoad: \$error');
|
||||
// ad.dispose();
|
||||
// },
|
||||
// onAdOpened: (ad) => print('\$BannerAd onAdOpened.'),
|
||||
// onAdClosed: (ad) => print('\$BannerAd onAdClosed.'),
|
||||
// ),
|
||||
// );
|
||||
// await banner.load();
|
||||
|
||||
// adWidget = AdWidget(ad: banner);
|
||||
// setState(() {});
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
106
lib/src/widgets/flutter_flow_autocomplete_options_list.dart
Normal file
106
lib/src/widgets/flutter_flow_autocomplete_options_list.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:substring_highlight/substring_highlight.dart';
|
||||
|
||||
/// A widget that displays a list of autocomplete options for a text field.
|
||||
class AutocompleteOptionsList extends StatelessWidget {
|
||||
const AutocompleteOptionsList({
|
||||
super.key,
|
||||
required this.textFieldKey,
|
||||
required this.textController,
|
||||
required this.options,
|
||||
required this.onSelected,
|
||||
required this.textStyle,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.optionBackgroundColor,
|
||||
this.optionHighlightColor,
|
||||
this.textHighlightStyle,
|
||||
this.maxHeight,
|
||||
this.elevation = 4.0,
|
||||
});
|
||||
|
||||
/// The key of the text field associated with the autocomplete options list.
|
||||
final GlobalKey textFieldKey;
|
||||
|
||||
/// The controller for the text field.
|
||||
final TextEditingController textController;
|
||||
|
||||
/// The list of autocomplete options.
|
||||
final List<String> options;
|
||||
|
||||
/// The callback function that is called when an option is selected.
|
||||
final Function(String) onSelected;
|
||||
|
||||
/// The color used to highlight the selected option.
|
||||
final Color? optionHighlightColor;
|
||||
|
||||
/// The background color of the options.
|
||||
final Color? optionBackgroundColor;
|
||||
|
||||
/// The style of the text in the options.
|
||||
final TextStyle textStyle;
|
||||
|
||||
/// The style of the highlighted text in the options.
|
||||
final TextStyle? textHighlightStyle;
|
||||
|
||||
/// The alignment of the text in the options.
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// The maximum height of the options list.
|
||||
final double? maxHeight;
|
||||
|
||||
/// The elevation of the options list.
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textFieldBox =
|
||||
textFieldKey.currentContext!.findRenderObject() as RenderBox;
|
||||
final textFieldWidth = textFieldBox.size.width;
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Material(
|
||||
elevation: elevation,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: textFieldWidth,
|
||||
maxHeight: maxHeight ?? 200,
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (context, index) {
|
||||
final option = options.elementAt(index);
|
||||
return InkWell(
|
||||
onTap: () => onSelected(option),
|
||||
child: Builder(builder: (context) {
|
||||
final bool highlight =
|
||||
AutocompleteHighlightedOption.of(context) == index;
|
||||
if (highlight) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
Scrollable.ensureVisible(context, alignment: 0.5);
|
||||
});
|
||||
}
|
||||
return Container(
|
||||
color: highlight
|
||||
? optionHighlightColor ?? Theme.of(context).focusColor
|
||||
: optionBackgroundColor,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SubstringHighlight(
|
||||
text: option,
|
||||
term: textController.text,
|
||||
textStyle: textStyle,
|
||||
textAlign: textAlign,
|
||||
textStyleHighlight: textHighlightStyle ?? textStyle,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
343
lib/src/widgets/flutter_flow_button.dart
Normal file
343
lib/src/widgets/flutter_flow_button.dart
Normal file
@@ -0,0 +1,343 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
class FFButtonOptions {
|
||||
const FFButtonOptions({
|
||||
this.textAlign,
|
||||
this.textStyle,
|
||||
this.elevation,
|
||||
this.height,
|
||||
this.width,
|
||||
this.padding,
|
||||
this.color,
|
||||
this.disabledColor,
|
||||
this.disabledTextColor,
|
||||
this.splashColor,
|
||||
this.iconSize,
|
||||
this.iconColor,
|
||||
this.iconPadding,
|
||||
this.borderRadius,
|
||||
this.borderSide,
|
||||
this.hoverColor,
|
||||
this.hoverBorderSide,
|
||||
this.hoverTextColor,
|
||||
this.hoverElevation,
|
||||
this.maxLines,
|
||||
});
|
||||
|
||||
/// The alignment of the button's text within its bounds.
|
||||
final TextAlign? textAlign;
|
||||
|
||||
/// The style of the button's text.
|
||||
final TextStyle? textStyle;
|
||||
|
||||
/// The elevation of the button.
|
||||
final double? elevation;
|
||||
|
||||
/// The height of the button.
|
||||
final double? height;
|
||||
|
||||
/// The width of the button.
|
||||
final double? width;
|
||||
|
||||
/// The padding around the button's content.
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// The background color of the button.
|
||||
final Color? color;
|
||||
|
||||
/// The background color of the button when it is disabled.
|
||||
final Color? disabledColor;
|
||||
|
||||
/// The text color of the button when it is disabled.
|
||||
final Color? disabledTextColor;
|
||||
|
||||
/// The maximum number of lines for the button's text.
|
||||
final int? maxLines;
|
||||
|
||||
/// The color of the splash effect when the button is pressed.
|
||||
final Color? splashColor;
|
||||
|
||||
/// The size of the button's icon.
|
||||
final double? iconSize;
|
||||
|
||||
/// The color of the button's icon.
|
||||
final Color? iconColor;
|
||||
|
||||
/// The padding around the button's icon.
|
||||
final EdgeInsetsGeometry? iconPadding;
|
||||
|
||||
/// The border radius of the button.
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
/// The border of the button.
|
||||
final BorderSide? borderSide;
|
||||
|
||||
/// The background color of the button when it is hovered.
|
||||
final Color? hoverColor;
|
||||
|
||||
/// The border of the button when it is hovered.
|
||||
final BorderSide? hoverBorderSide;
|
||||
|
||||
/// The text color of the button when it is hovered.
|
||||
final Color? hoverTextColor;
|
||||
|
||||
/// The elevation of the button when it is hovered.
|
||||
final double? hoverElevation;
|
||||
}
|
||||
|
||||
/// A customizable button widget that can display text, an icon, and a loading indicator.
|
||||
class FFButtonWidget extends StatefulWidget {
|
||||
/// Creates a [FFButtonWidget].
|
||||
///
|
||||
/// - [text] parameter is required and specifies the text to be displayed on the button.
|
||||
/// - [onPressed] parameter is a callback function that will be called when the button is pressed.
|
||||
/// - [icon] parameter is an optional widget that can be used to display an icon alongside the text.
|
||||
/// - [iconData] parameter is an optional icon data that can be used to display an icon alongside the text.
|
||||
/// - [options] parameter is required and specifies the visual options for the button.
|
||||
/// - [showLoadingIndicator] parameter is an optional boolean value that determines whether to show a loading indicator when the button is pressed.
|
||||
const FFButtonWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onPressed,
|
||||
this.icon,
|
||||
this.iconData,
|
||||
required this.options,
|
||||
this.showLoadingIndicator = true,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final Widget? icon;
|
||||
final IconData? iconData;
|
||||
final Function()? onPressed;
|
||||
final FFButtonOptions options;
|
||||
final bool showLoadingIndicator;
|
||||
|
||||
@override
|
||||
State<FFButtonWidget> createState() => _FFButtonWidgetState();
|
||||
}
|
||||
|
||||
class _FFButtonWidgetState extends State<FFButtonWidget> {
|
||||
bool loading = false;
|
||||
|
||||
int get maxLines => widget.options.maxLines ?? 1;
|
||||
|
||||
String? get text =>
|
||||
widget.options.textStyle?.fontSize == 0 ? null : widget.text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget textWidget = loading
|
||||
? SizedBox(
|
||||
width: widget.options.width == null
|
||||
? _getTextWidth(text, widget.options.textStyle, maxLines)
|
||||
: null,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 23,
|
||||
height: 23,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
widget.options.textStyle?.color ?? Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: AutoSizeText(
|
||||
text ?? '',
|
||||
style:
|
||||
text == null ? null : widget.options.textStyle?.withoutColor(),
|
||||
textAlign: widget.options.textAlign,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
|
||||
final onPressed = widget.onPressed != null
|
||||
? (widget.showLoadingIndicator
|
||||
? () async {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setState(() => loading = true);
|
||||
try {
|
||||
await widget.onPressed!();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
: () => widget.onPressed!())
|
||||
: null;
|
||||
|
||||
ButtonStyle style = ButtonStyle(
|
||||
shape: WidgetStateProperty.resolveWith<OutlinedBorder>(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.hovered) &&
|
||||
widget.options.hoverBorderSide != null) {
|
||||
return RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
widget.options.borderRadius ?? BorderRadius.circular(8),
|
||||
side: widget.options.hoverBorderSide!,
|
||||
);
|
||||
}
|
||||
return RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
widget.options.borderRadius ?? BorderRadius.circular(8),
|
||||
side: widget.options.borderSide ?? BorderSide.none,
|
||||
);
|
||||
},
|
||||
),
|
||||
foregroundColor: WidgetStateProperty.resolveWith<Color?>(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.disabled) &&
|
||||
widget.options.disabledTextColor != null) {
|
||||
return widget.options.disabledTextColor;
|
||||
}
|
||||
if (states.contains(WidgetState.hovered) &&
|
||||
widget.options.hoverTextColor != null) {
|
||||
return widget.options.hoverTextColor;
|
||||
}
|
||||
return widget.options.textStyle?.color ?? Colors.white;
|
||||
},
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.resolveWith<Color?>(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.disabled) &&
|
||||
widget.options.disabledColor != null) {
|
||||
return widget.options.disabledColor;
|
||||
}
|
||||
if (states.contains(WidgetState.hovered) &&
|
||||
widget.options.hoverColor != null) {
|
||||
return widget.options.hoverColor;
|
||||
}
|
||||
return widget.options.color;
|
||||
},
|
||||
),
|
||||
overlayColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return widget.options.splashColor;
|
||||
}
|
||||
return widget.options.hoverColor == null ? null : Colors.transparent;
|
||||
}),
|
||||
padding: WidgetStateProperty.all(widget.options.padding ??
|
||||
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0)),
|
||||
elevation: WidgetStateProperty.resolveWith<double?>(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.hovered) &&
|
||||
widget.options.hoverElevation != null) {
|
||||
return widget.options.hoverElevation!;
|
||||
}
|
||||
return widget.options.elevation ?? 2.0;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if ((widget.icon != null || widget.iconData != null) && !loading) {
|
||||
Widget icon = widget.icon ??
|
||||
FaIcon(
|
||||
widget.iconData!,
|
||||
size: widget.options.iconSize,
|
||||
color: widget.options.iconColor,
|
||||
);
|
||||
|
||||
if (text == null) {
|
||||
return Container(
|
||||
height: widget.options.height,
|
||||
width: widget.options.width,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
widget.options.borderSide ?? BorderSide.none,
|
||||
),
|
||||
borderRadius:
|
||||
widget.options.borderRadius ?? BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
splashRadius: 1.0,
|
||||
icon: Padding(
|
||||
padding: widget.options.iconPadding ?? EdgeInsets.zero,
|
||||
child: icon,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: widget.options.height,
|
||||
width: widget.options.width,
|
||||
child: ElevatedButton.icon(
|
||||
icon: Padding(
|
||||
padding: widget.options.iconPadding ?? EdgeInsets.zero,
|
||||
child: icon,
|
||||
),
|
||||
label: textWidget,
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: widget.options.height,
|
||||
width: widget.options.width,
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
child: textWidget,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension on [TextStyle] to create a new [TextStyle] without the color property.
|
||||
/// This extension method returns a new [TextStyle] object with all properties of the original [TextStyle] except for the color property, which is set to null.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// TextStyle myTextStyle = TextStyle(color: Colors.red, fontSize: 16);
|
||||
/// TextStyle newTextStyle = myTextStyle.withoutColor();
|
||||
/// ```
|
||||
extension _WithoutColorExtension on TextStyle {
|
||||
/// Returns a new [TextStyle] object without the color property.
|
||||
TextStyle withoutColor() => TextStyle(
|
||||
inherit: inherit,
|
||||
color: null,
|
||||
backgroundColor: backgroundColor,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
fontStyle: fontStyle,
|
||||
letterSpacing: letterSpacing,
|
||||
wordSpacing: wordSpacing,
|
||||
textBaseline: textBaseline,
|
||||
height: height,
|
||||
leadingDistribution: leadingDistribution,
|
||||
locale: locale,
|
||||
foreground: foreground,
|
||||
background: background,
|
||||
shadows: shadows,
|
||||
fontFeatures: fontFeatures,
|
||||
decoration: decoration,
|
||||
decorationColor: decorationColor,
|
||||
decorationStyle: decorationStyle,
|
||||
decorationThickness: decorationThickness,
|
||||
debugLabel: debugLabel,
|
||||
fontFamily: fontFamily,
|
||||
fontFamilyFallback: fontFamilyFallback,
|
||||
overflow: overflow,
|
||||
);
|
||||
}
|
||||
|
||||
// Slightly hacky method of getting the layout width of the provided text.
|
||||
double? _getTextWidth(String? text, TextStyle? style, int maxLines) =>
|
||||
text != null
|
||||
? (TextPainter(
|
||||
text: TextSpan(text: text, style: style),
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: maxLines,
|
||||
)..layout())
|
||||
.size
|
||||
.width
|
||||
: null;
|
||||
854
lib/src/widgets/flutter_flow_button_tabbar.dart
Normal file
854
lib/src/widgets/flutter_flow_button_tabbar.dart
Normal file
@@ -0,0 +1,854 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
const double _kTabHeight = 46.0;
|
||||
|
||||
typedef _LayoutCallback = void Function(
|
||||
List<double> xOffsets, TextDirection textDirection, double width);
|
||||
|
||||
class _TabLabelBarRenderer extends RenderFlex {
|
||||
_TabLabelBarRenderer({
|
||||
required super.direction,
|
||||
required super.mainAxisSize,
|
||||
required super.mainAxisAlignment,
|
||||
required super.crossAxisAlignment,
|
||||
required TextDirection super.textDirection,
|
||||
required super.verticalDirection,
|
||||
required this.onPerformLayout,
|
||||
});
|
||||
|
||||
_LayoutCallback onPerformLayout;
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
super.performLayout();
|
||||
// xOffsets will contain childCount+1 values, giving the offsets of the
|
||||
// leading edge of the first tab as the first value, of the leading edge of
|
||||
// the each subsequent tab as each subsequent value, and of the trailing
|
||||
// edge of the last tab as the last value.
|
||||
RenderBox? child = firstChild;
|
||||
final List<double> xOffsets = <double>[];
|
||||
while (child != null) {
|
||||
final FlexParentData childParentData =
|
||||
child.parentData! as FlexParentData;
|
||||
xOffsets.add(childParentData.offset.dx);
|
||||
assert(child.parentData == childParentData);
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
assert(textDirection != null);
|
||||
switch (textDirection!) {
|
||||
case TextDirection.rtl:
|
||||
xOffsets.insert(0, size.width);
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
xOffsets.add(size.width);
|
||||
break;
|
||||
}
|
||||
onPerformLayout(xOffsets, textDirection!, size.width);
|
||||
}
|
||||
}
|
||||
|
||||
// This class and its renderer class only exist to report the widths of the tabs
|
||||
// upon layout. The tab widths are only used at paint time (see _IndicatorPainter)
|
||||
// or in response to input.
|
||||
class _TabLabelBar extends Flex {
|
||||
const _TabLabelBar({
|
||||
required super.children,
|
||||
required this.onPerformLayout,
|
||||
}) : super(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
verticalDirection: VerticalDirection.down,
|
||||
);
|
||||
|
||||
final _LayoutCallback onPerformLayout;
|
||||
|
||||
@override
|
||||
RenderFlex createRenderObject(BuildContext context) {
|
||||
return _TabLabelBarRenderer(
|
||||
direction: direction,
|
||||
mainAxisAlignment: mainAxisAlignment,
|
||||
mainAxisSize: mainAxisSize,
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
textDirection: getEffectiveTextDirection(context)!,
|
||||
verticalDirection: verticalDirection,
|
||||
onPerformLayout: onPerformLayout,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context, _TabLabelBarRenderer renderObject) {
|
||||
super.updateRenderObject(context, renderObject);
|
||||
renderObject.onPerformLayout = onPerformLayout;
|
||||
}
|
||||
}
|
||||
|
||||
class _IndicatorPainter extends CustomPainter {
|
||||
_IndicatorPainter({
|
||||
required this.controller,
|
||||
required this.tabKeys,
|
||||
required _IndicatorPainter? old,
|
||||
}) : super(repaint: controller.animation) {
|
||||
if (old != null) {
|
||||
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
|
||||
}
|
||||
}
|
||||
|
||||
final TabController controller;
|
||||
|
||||
final List<GlobalKey> tabKeys;
|
||||
|
||||
// _currentTabOffsets and _currentTextDirection are set each time TabBar
|
||||
// layout is completed. These values can be null when TabBar contains no
|
||||
// tabs, since there are nothing to lay out.
|
||||
List<double>? _currentTabOffsets;
|
||||
TextDirection? _currentTextDirection;
|
||||
|
||||
BoxPainter? _painter;
|
||||
bool _needsPaint = false;
|
||||
void markNeedsPaint() {
|
||||
_needsPaint = true;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_painter?.dispose();
|
||||
}
|
||||
|
||||
void saveTabOffsets(List<double>? tabOffsets, TextDirection? textDirection) {
|
||||
_currentTabOffsets = tabOffsets;
|
||||
_currentTextDirection = textDirection;
|
||||
}
|
||||
|
||||
// _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
|
||||
// _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
|
||||
int get maxTabIndex => _currentTabOffsets!.length - 2;
|
||||
|
||||
double centerOf(int tabIndex) {
|
||||
assert(_currentTabOffsets != null);
|
||||
assert(_currentTabOffsets!.isNotEmpty);
|
||||
assert(tabIndex >= 0);
|
||||
assert(tabIndex <= maxTabIndex);
|
||||
return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) /
|
||||
2.0;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
_needsPaint = false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_IndicatorPainter old) {
|
||||
return _needsPaint ||
|
||||
controller != old.controller ||
|
||||
tabKeys.length != old.tabKeys.length ||
|
||||
(!listEquals(_currentTabOffsets, old._currentTabOffsets)) ||
|
||||
_currentTextDirection != old._currentTextDirection;
|
||||
}
|
||||
}
|
||||
|
||||
// This class, and TabBarScrollController, only exist to handle the case
|
||||
// where a scrollable TabBar has a non-zero initialIndex. In that case we can
|
||||
// only compute the scroll position's initial scroll offset (the "correct"
|
||||
// pixels value) after the TabBar viewport width and scroll limits are known.
|
||||
|
||||
class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
|
||||
_TabBarScrollPosition({
|
||||
required super.physics,
|
||||
required super.context,
|
||||
required super.oldPosition,
|
||||
required this.tabBar,
|
||||
}) : super(
|
||||
initialPixels: null,
|
||||
);
|
||||
|
||||
final _FlutterFlowButtonTabBarState tabBar;
|
||||
|
||||
bool _viewportDimensionWasNonZero = false;
|
||||
|
||||
// Position should be adjusted at least once.
|
||||
bool _needsPixelsCorrection = true;
|
||||
|
||||
@override
|
||||
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
|
||||
bool result = true;
|
||||
if (!_viewportDimensionWasNonZero) {
|
||||
_viewportDimensionWasNonZero = viewportDimension != 0.0;
|
||||
}
|
||||
// If the viewport never had a non-zero dimension, we just want to jump
|
||||
// to the initial scroll position to avoid strange scrolling effects in
|
||||
// release mode: In release mode, the viewport temporarily may have a
|
||||
// dimension of zero before the actual dimension is calculated. In that
|
||||
// scenario, setting the actual dimension would cause a strange scroll
|
||||
// effect without this guard because the super call below would starts a
|
||||
// ballistic scroll activity.
|
||||
if (!_viewportDimensionWasNonZero || _needsPixelsCorrection) {
|
||||
_needsPixelsCorrection = false;
|
||||
correctPixels(tabBar._initialScrollOffset(
|
||||
viewportDimension, minScrollExtent, maxScrollExtent));
|
||||
result = false;
|
||||
}
|
||||
return super.applyContentDimensions(minScrollExtent, maxScrollExtent) &&
|
||||
result;
|
||||
}
|
||||
|
||||
void markNeedsPixelsCorrection() {
|
||||
_needsPixelsCorrection = true;
|
||||
}
|
||||
}
|
||||
|
||||
// This class, and TabBarScrollPosition, only exist to handle the case
|
||||
// where a scrollable TabBar has a non-zero initialIndex.
|
||||
class _TabBarScrollController extends ScrollController {
|
||||
_TabBarScrollController(this.tabBar);
|
||||
|
||||
final _FlutterFlowButtonTabBarState tabBar;
|
||||
|
||||
@override
|
||||
ScrollPosition createScrollPosition(ScrollPhysics physics,
|
||||
ScrollContext context, ScrollPosition? oldPosition) {
|
||||
return _TabBarScrollPosition(
|
||||
physics: physics,
|
||||
context: context,
|
||||
oldPosition: oldPosition,
|
||||
tabBar: tabBar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A Flutterflow Design widget that displays a horizontal row of tabs.
|
||||
class FlutterFlowButtonTabBar extends StatefulWidget
|
||||
implements PreferredSizeWidget {
|
||||
/// The [tabs] argument must not be null and its length must match the [controller]'s
|
||||
/// [TabController.length].
|
||||
///
|
||||
/// If a [TabController] is not provided, then there must be a
|
||||
/// [DefaultTabController] ancestor.
|
||||
///
|
||||
const FlutterFlowButtonTabBar({
|
||||
super.key,
|
||||
required this.tabs,
|
||||
this.controller,
|
||||
this.isScrollable = false,
|
||||
this.useToggleButtonStyle = false,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.onTap,
|
||||
this.backgroundColor,
|
||||
this.unselectedBackgroundColor,
|
||||
this.decoration,
|
||||
this.unselectedDecoration,
|
||||
this.labelStyle,
|
||||
this.unselectedLabelStyle,
|
||||
this.labelColor,
|
||||
this.unselectedLabelColor,
|
||||
this.borderWidth = 0,
|
||||
this.borderColor = Colors.transparent,
|
||||
this.unselectedBorderColor = Colors.transparent,
|
||||
this.physics = const BouncingScrollPhysics(),
|
||||
this.labelPadding = const EdgeInsets.symmetric(horizontal: 4),
|
||||
this.buttonMargin = const EdgeInsets.all(4),
|
||||
this.padding = EdgeInsets.zero,
|
||||
this.borderRadius = 8.0,
|
||||
this.elevation = 0,
|
||||
});
|
||||
|
||||
/// Typically a list of two or more [Tab] widgets.
|
||||
///
|
||||
/// The length of this list must match the [controller]'s [TabController.length]
|
||||
/// and the length of the [TabBarView.children] list.
|
||||
final List<Widget> tabs;
|
||||
|
||||
/// This widget's selection and animation state.
|
||||
///
|
||||
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
|
||||
/// will be used.
|
||||
final TabController? controller;
|
||||
|
||||
/// Whether this tab bar can be scrolled horizontally.
|
||||
///
|
||||
/// If [isScrollable] is true, then each tab is as wide as needed for its label
|
||||
/// and the entire [FlutterFlowButtonTabBar] is scrollable. Otherwise each tab gets an equal
|
||||
/// share of the available space.
|
||||
final bool isScrollable;
|
||||
|
||||
/// Whether the tab buttons should be styled as toggle buttons.
|
||||
final bool useToggleButtonStyle;
|
||||
|
||||
/// The background [Color] of the button on its selected state.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The background [Color] of the button on its unselected state.
|
||||
final Color? unselectedBackgroundColor;
|
||||
|
||||
/// The [BoxDecoration] of the button on its selected state.
|
||||
///
|
||||
/// If [BoxDecoration] is not provided, [backgroundColor] is used.
|
||||
final BoxDecoration? decoration;
|
||||
|
||||
/// The [BoxDecoration] of the button on its unselected state.
|
||||
///
|
||||
/// If [BoxDecoration] is not provided, [unselectedBackgroundColor] is used.
|
||||
final BoxDecoration? unselectedDecoration;
|
||||
|
||||
/// The [TextStyle] of the button's [Text] on its selected state. The color provided
|
||||
/// on the TextStyle will be used for the [Icon]'s color.
|
||||
final TextStyle? labelStyle;
|
||||
|
||||
/// The color of selected tab labels.
|
||||
final Color? labelColor;
|
||||
|
||||
/// The color of unselected tab labels.
|
||||
final Color? unselectedLabelColor;
|
||||
|
||||
/// The [TextStyle] of the button's [Text] on its unselected state. The color provided
|
||||
/// on the TextStyle will be used for the [Icon]'s color.
|
||||
final TextStyle? unselectedLabelStyle;
|
||||
|
||||
/// The with of solid [Border] for each button. If no value is provided, the border
|
||||
/// is not drawn.
|
||||
final double borderWidth;
|
||||
|
||||
/// The [Color] of solid [Border] for each button.
|
||||
final Color? borderColor;
|
||||
|
||||
/// The [Color] of solid [Border] for each button. If no value is provided, the value of
|
||||
/// [this.borderColor] is used.
|
||||
final Color? unselectedBorderColor;
|
||||
|
||||
/// The [EdgeInsets] used for the [Padding] of the buttons' content.
|
||||
///
|
||||
/// The default value is [EdgeInsets.symmetric(horizontal: 4)].
|
||||
final EdgeInsetsGeometry labelPadding;
|
||||
|
||||
/// The [EdgeInsets] used for the [Margin] of the buttons.
|
||||
///
|
||||
/// The default value is [EdgeInsets.all(4)].
|
||||
final EdgeInsetsGeometry buttonMargin;
|
||||
|
||||
/// The amount of space by which to inset the tab bar.
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// The value of the [BorderRadius.circular] applied to each button.
|
||||
final double borderRadius;
|
||||
|
||||
/// The value of the [elevation] applied to each button.
|
||||
final double elevation;
|
||||
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
final ValueChanged<int>? onTap;
|
||||
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
/// A size whose height depends on if the tabs have both icons and text.
|
||||
///
|
||||
/// [AppBar] uses this size to compute its own preferred size.
|
||||
@override
|
||||
Size get preferredSize {
|
||||
double maxHeight = _kTabHeight;
|
||||
for (final Widget item in tabs) {
|
||||
if (item is PreferredSizeWidget) {
|
||||
final double itemHeight = item.preferredSize.height;
|
||||
maxHeight = math.max(itemHeight, maxHeight);
|
||||
}
|
||||
}
|
||||
return Size.fromHeight(
|
||||
maxHeight + labelPadding.vertical + buttonMargin.vertical);
|
||||
}
|
||||
|
||||
@override
|
||||
State<FlutterFlowButtonTabBar> createState() =>
|
||||
_FlutterFlowButtonTabBarState();
|
||||
}
|
||||
|
||||
class _FlutterFlowButtonTabBarState extends State<FlutterFlowButtonTabBar>
|
||||
with TickerProviderStateMixin {
|
||||
ScrollController? _scrollController;
|
||||
TabController? _controller;
|
||||
_IndicatorPainter? _indicatorPainter;
|
||||
late AnimationController _animationController;
|
||||
int _currentIndex = 0;
|
||||
int _prevIndex = -1;
|
||||
|
||||
late double _tabStripWidth;
|
||||
late List<GlobalKey> _tabKeys;
|
||||
|
||||
final GlobalKey _tabsParentKey = GlobalKey();
|
||||
|
||||
bool _debugHasScheduledValidTabsCountCheck = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find
|
||||
// the width of tab widget i. See _IndicatorPainter.indicatorRect().
|
||||
_tabKeys = widget.tabs.map((tab) => GlobalKey()).toList();
|
||||
|
||||
/// The animation duration is 2/3 of the tab scroll animation duration in
|
||||
/// Material design (kTabScrollDuration).
|
||||
_animationController = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 200));
|
||||
|
||||
// so the buttons start in their "final" state (color)
|
||||
_animationController
|
||||
..value = 1.0
|
||||
..addListener(() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If the TabBar is rebuilt with a new tab controller, the caller should
|
||||
// dispose the old one. In that case the old controller's animation will be
|
||||
// null and should not be accessed.
|
||||
bool get _controllerIsValid => _controller?.animation != null;
|
||||
|
||||
void _updateTabController() {
|
||||
final TabController? newController =
|
||||
widget.controller ?? DefaultTabController.maybeOf(context);
|
||||
assert(() {
|
||||
if (newController == null) {
|
||||
throw FlutterError(
|
||||
'No TabController for \${widget.runtimeType}.\\n'
|
||||
'When creating a \${widget.runtimeType}, you must either provide an explicit '
|
||||
'TabController using the "controller" property, or you must ensure that there '
|
||||
'is a DefaultTabController above the \${widget.runtimeType}.\\n'
|
||||
'In this case, there was neither an explicit controller nor a default controller.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (newController == _controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_controllerIsValid) {
|
||||
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||
_controller!.removeListener(_handleTabControllerTick);
|
||||
}
|
||||
_controller = newController;
|
||||
if (_controller != null) {
|
||||
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
|
||||
_controller!.addListener(_handleTabControllerTick);
|
||||
_currentIndex = _controller!.index;
|
||||
}
|
||||
}
|
||||
|
||||
void _initIndicatorPainter() {
|
||||
_indicatorPainter = !_controllerIsValid
|
||||
? null
|
||||
: _IndicatorPainter(
|
||||
controller: _controller!,
|
||||
tabKeys: _tabKeys,
|
||||
old: _indicatorPainter,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
assert(debugCheckHasMaterial(context));
|
||||
_updateTabController();
|
||||
_initIndicatorPainter();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FlutterFlowButtonTabBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
_updateTabController();
|
||||
_initIndicatorPainter();
|
||||
// Adjust scroll position.
|
||||
if (_scrollController != null) {
|
||||
final ScrollPosition position = _scrollController!.position;
|
||||
if (position is _TabBarScrollPosition) {
|
||||
position.markNeedsPixelsCorrection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.tabs.length > _tabKeys.length) {
|
||||
final int delta = widget.tabs.length - _tabKeys.length;
|
||||
_tabKeys.addAll(List<GlobalKey>.generate(delta, (n) => GlobalKey()));
|
||||
} else if (widget.tabs.length < _tabKeys.length) {
|
||||
_tabKeys.removeRange(widget.tabs.length, _tabKeys.length);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_indicatorPainter!.dispose();
|
||||
if (_controllerIsValid) {
|
||||
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||
_controller!.removeListener(_handleTabControllerTick);
|
||||
}
|
||||
_controller = null;
|
||||
// We don't own the _controller Animation, so it's not disposed here.
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int get maxTabIndex => _indicatorPainter!.maxTabIndex;
|
||||
|
||||
double _tabScrollOffset(
|
||||
int index, double viewportWidth, double minExtent, double maxExtent) {
|
||||
if (!widget.isScrollable) {
|
||||
return 0.0;
|
||||
}
|
||||
double tabCenter = _indicatorPainter!.centerOf(index);
|
||||
double paddingStart;
|
||||
switch (Directionality.of(context)) {
|
||||
case TextDirection.rtl:
|
||||
paddingStart = widget.padding?.resolve(TextDirection.rtl).right ?? 0;
|
||||
tabCenter = _tabStripWidth - tabCenter;
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
paddingStart = widget.padding?.resolve(TextDirection.ltr).left ?? 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return clampDouble(
|
||||
tabCenter + paddingStart - viewportWidth / 2.0, minExtent, maxExtent);
|
||||
}
|
||||
|
||||
double _tabCenteredScrollOffset(int index) {
|
||||
final ScrollPosition position = _scrollController!.position;
|
||||
return _tabScrollOffset(index, position.viewportDimension,
|
||||
position.minScrollExtent, position.maxScrollExtent);
|
||||
}
|
||||
|
||||
double _initialScrollOffset(
|
||||
double viewportWidth, double minExtent, double maxExtent) {
|
||||
return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent);
|
||||
}
|
||||
|
||||
void _scrollToCurrentIndex() {
|
||||
final double offset = _tabCenteredScrollOffset(_currentIndex);
|
||||
_scrollController!
|
||||
.animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);
|
||||
}
|
||||
|
||||
void _scrollToControllerValue() {
|
||||
final double? leadingPosition =
|
||||
_currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null;
|
||||
final double middlePosition = _tabCenteredScrollOffset(_currentIndex);
|
||||
final double? trailingPosition = _currentIndex < maxTabIndex
|
||||
? _tabCenteredScrollOffset(_currentIndex + 1)
|
||||
: null;
|
||||
|
||||
final double index = _controller!.index.toDouble();
|
||||
final double value = _controller!.animation!.value;
|
||||
final double offset;
|
||||
if (value == index - 1.0) {
|
||||
offset = leadingPosition ?? middlePosition;
|
||||
} else if (value == index + 1.0) {
|
||||
offset = trailingPosition ?? middlePosition;
|
||||
} else if (value == index) {
|
||||
offset = middlePosition;
|
||||
} else if (value < index) {
|
||||
offset = leadingPosition == null
|
||||
? middlePosition
|
||||
: lerpDouble(middlePosition, leadingPosition, index - value)!;
|
||||
} else {
|
||||
offset = trailingPosition == null
|
||||
? middlePosition
|
||||
: lerpDouble(middlePosition, trailingPosition, value - index)!;
|
||||
}
|
||||
|
||||
_scrollController!.jumpTo(offset);
|
||||
}
|
||||
|
||||
void _handleTabControllerAnimationTick() {
|
||||
assert(mounted);
|
||||
if (!_controller!.indexIsChanging && widget.isScrollable) {
|
||||
// Sync the TabBar's scroll position with the TabBarView's PageView.
|
||||
_currentIndex = _controller!.index;
|
||||
_scrollToControllerValue();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTabControllerTick() {
|
||||
if (_controller!.index != _currentIndex) {
|
||||
_prevIndex = _currentIndex;
|
||||
_currentIndex = _controller!.index;
|
||||
_triggerAnimation();
|
||||
if (widget.isScrollable) {
|
||||
_scrollToCurrentIndex();
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
// Rebuild the tabs after a (potentially animated) index change
|
||||
// has completed.
|
||||
});
|
||||
}
|
||||
|
||||
void _triggerAnimation() {
|
||||
// reset the animation so it's ready to go
|
||||
_animationController
|
||||
..reset()
|
||||
..forward();
|
||||
}
|
||||
|
||||
// Called each time layout completes.
|
||||
void _saveTabOffsets(
|
||||
List<double> tabOffsets, TextDirection textDirection, double width) {
|
||||
_tabStripWidth = width;
|
||||
_indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);
|
||||
}
|
||||
|
||||
void _handleTap(int index) {
|
||||
assert(index >= 0 && index < widget.tabs.length);
|
||||
_controller?.animateTo(index);
|
||||
widget.onTap?.call(index);
|
||||
}
|
||||
|
||||
Widget _buildStyledTab(Widget child, int index) {
|
||||
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
||||
|
||||
final double animationValue;
|
||||
if (index == _currentIndex) {
|
||||
animationValue = _animationController.value;
|
||||
} else if (index == _prevIndex) {
|
||||
animationValue = 1 - _animationController.value;
|
||||
} else {
|
||||
animationValue = 0;
|
||||
}
|
||||
|
||||
final TextStyle? textStyle = TextStyle.lerp(
|
||||
(widget.unselectedLabelStyle ??
|
||||
tabBarTheme.labelStyle ??
|
||||
DefaultTextStyle.of(context).style)
|
||||
.copyWith(
|
||||
color: widget.unselectedLabelColor,
|
||||
),
|
||||
(widget.labelStyle ??
|
||||
tabBarTheme.labelStyle ??
|
||||
DefaultTextStyle.of(context).style)
|
||||
.copyWith(
|
||||
color: widget.labelColor,
|
||||
),
|
||||
animationValue);
|
||||
|
||||
final Color? textColor = Color.lerp(
|
||||
widget.unselectedLabelColor, widget.labelColor, animationValue);
|
||||
|
||||
final Color? borderColor = Color.lerp(
|
||||
widget.unselectedBorderColor, widget.borderColor, animationValue);
|
||||
|
||||
BoxDecoration? boxDecoration = BoxDecoration.lerp(
|
||||
BoxDecoration(
|
||||
color: widget.unselectedDecoration?.color ??
|
||||
widget.unselectedBackgroundColor ??
|
||||
Colors.transparent,
|
||||
boxShadow: widget.unselectedDecoration?.boxShadow,
|
||||
gradient: widget.unselectedDecoration?.gradient,
|
||||
borderRadius: widget.useToggleButtonStyle
|
||||
? null
|
||||
: BorderRadius.circular(widget.borderRadius),
|
||||
),
|
||||
BoxDecoration(
|
||||
color: widget.decoration?.color ??
|
||||
widget.backgroundColor ??
|
||||
Colors.transparent,
|
||||
boxShadow: widget.decoration?.boxShadow,
|
||||
gradient: widget.decoration?.gradient,
|
||||
borderRadius: widget.useToggleButtonStyle
|
||||
? null
|
||||
: BorderRadius.circular(widget.borderRadius),
|
||||
),
|
||||
animationValue);
|
||||
|
||||
if (widget.useToggleButtonStyle &&
|
||||
widget.borderWidth > 0 &&
|
||||
boxDecoration != null) {
|
||||
if (index == 0) {
|
||||
boxDecoration = boxDecoration.copyWith(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: widget.unselectedBorderColor ?? Colors.transparent,
|
||||
width: widget.borderWidth / 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (index == widget.tabs.length - 1) {
|
||||
boxDecoration = boxDecoration.copyWith(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: widget.unselectedBorderColor ?? Colors.transparent,
|
||||
width: widget.borderWidth / 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
boxDecoration = boxDecoration.copyWith(
|
||||
border: Border.symmetric(
|
||||
vertical: BorderSide(
|
||||
color: widget.unselectedBorderColor ?? Colors.transparent,
|
||||
width: widget.borderWidth / 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
key: _tabKeys[index],
|
||||
// padding for the buttons
|
||||
padding:
|
||||
widget.useToggleButtonStyle ? EdgeInsets.zero : widget.buttonMargin,
|
||||
child: TextButton(
|
||||
onPressed: () => _handleTap(index),
|
||||
style: ButtonStyle(
|
||||
elevation: WidgetStateProperty.all(
|
||||
widget.useToggleButtonStyle ? 0 : widget.elevation),
|
||||
|
||||
/// give a pretty small minimum size
|
||||
minimumSize: WidgetStateProperty.all(const Size(10, 10)),
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
textStyle: WidgetStateProperty.all(textStyle),
|
||||
foregroundColor: WidgetStateProperty.all(textColor),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
shape: WidgetStateProperty.all(
|
||||
widget.useToggleButtonStyle
|
||||
? const RoundedRectangleBorder(
|
||||
side: BorderSide.none,
|
||||
borderRadius: BorderRadius.zero,
|
||||
)
|
||||
: RoundedRectangleBorder(
|
||||
side: (widget.borderWidth == 0)
|
||||
? BorderSide.none
|
||||
: BorderSide(
|
||||
color: borderColor ?? Colors.transparent,
|
||||
width: widget.borderWidth,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Ink(
|
||||
decoration: boxDecoration,
|
||||
child: Container(
|
||||
padding: widget.labelPadding,
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _debugScheduleCheckHasValidTabsCount() {
|
||||
if (_debugHasScheduledValidTabsCountCheck) {
|
||||
return true;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((duration) {
|
||||
_debugHasScheduledValidTabsCountCheck = false;
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
assert(() {
|
||||
if (_controller!.length != widget.tabs.length) {
|
||||
throw FlutterError(
|
||||
"Controller's length property (\${_controller!.length}) does not match the "
|
||||
"number of tabs (\${widget.tabs.length}) present in TabBar's tabs property.",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
});
|
||||
_debugHasScheduledValidTabsCountCheck = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(_debugScheduleCheckHasValidTabsCount());
|
||||
|
||||
if (_controller!.length == 0) {
|
||||
return Container(
|
||||
height: _kTabHeight +
|
||||
widget.labelPadding.vertical +
|
||||
widget.buttonMargin.vertical,
|
||||
);
|
||||
}
|
||||
|
||||
final List<Widget> wrappedTabs =
|
||||
List<Widget>.generate(widget.tabs.length, (index) {
|
||||
return _buildStyledTab(widget.tabs[index], index);
|
||||
});
|
||||
|
||||
final int tabCount = widget.tabs.length;
|
||||
// Add the tap handler to each tab. If the tab bar is not scrollable,
|
||||
// then give all of the tabs equal flexibility so that they each occupy
|
||||
// the same share of the tab bar's overall width.
|
||||
|
||||
for (int index = 0; index < tabCount; index += 1) {
|
||||
if (!widget.isScrollable) {
|
||||
wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
|
||||
}
|
||||
}
|
||||
|
||||
Widget tabBar = AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
key: _tabsParentKey,
|
||||
builder: (context, child) {
|
||||
Widget tabBarTemp = _TabLabelBar(
|
||||
onPerformLayout: _saveTabOffsets,
|
||||
children: wrappedTabs,
|
||||
);
|
||||
|
||||
if (widget.useToggleButtonStyle) {
|
||||
tabBarTemp = Material(
|
||||
shape: widget.useToggleButtonStyle
|
||||
? RoundedRectangleBorder(
|
||||
side: (widget.borderWidth == 0)
|
||||
? BorderSide.none
|
||||
: BorderSide(
|
||||
color: widget.borderColor ?? Colors.transparent,
|
||||
width: widget.borderWidth,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
)
|
||||
: null,
|
||||
elevation: widget.useToggleButtonStyle ? widget.elevation : 0,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
child: tabBarTemp,
|
||||
);
|
||||
}
|
||||
return CustomPaint(
|
||||
painter: _indicatorPainter,
|
||||
child: tabBarTemp,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.isScrollable) {
|
||||
_scrollController ??= _TabBarScrollController(this);
|
||||
tabBar = SingleChildScrollView(
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _scrollController,
|
||||
padding: widget.padding,
|
||||
physics: widget.physics,
|
||||
child: tabBar,
|
||||
);
|
||||
} else if (widget.padding != null) {
|
||||
tabBar = Padding(
|
||||
padding: widget.padding!,
|
||||
child: tabBar,
|
||||
);
|
||||
}
|
||||
|
||||
return tabBar;
|
||||
}
|
||||
}
|
||||
397
lib/src/widgets/flutter_flow_calendar.dart
Normal file
397
lib/src/widgets/flutter_flow_calendar.dart
Normal file
@@ -0,0 +1,397 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
DateTime kFirstDay = DateTime(1970, 1, 1);
|
||||
DateTime kLastDay = DateTime(2100, 1, 1);
|
||||
|
||||
extension DateTimeExtension on DateTime {
|
||||
DateTime get startOfDay => DateTime(year, month, day);
|
||||
|
||||
DateTime get endOfDay => DateTime(year, month, day, 23, 59);
|
||||
}
|
||||
|
||||
/// A customizable calendar widget for FlutterFlow.
|
||||
///
|
||||
/// The `FlutterFlowCalendar` widget allows you to display a calendar with various customization options.
|
||||
/// You can customize the color, date format, starting day of the week, header style, and more.
|
||||
///
|
||||
/// To use this widget, simply create an instance of `FlutterFlowCalendar` and pass in the desired parameters.
|
||||
/// You can also provide a callback function to handle changes in the selected date range.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// FlutterFlowCalendar(
|
||||
/// color: Colors.blue,
|
||||
/// onChange: (DateTimeRange? selectedRange) {
|
||||
/// // Handle selected date range change
|
||||
/// },
|
||||
/// initialDate: DateTime.now(),
|
||||
/// weekFormat: true,
|
||||
/// weekStartsMonday: true,
|
||||
/// twoRowHeader: true,
|
||||
/// iconColor: Colors.white,
|
||||
/// dateStyle: TextStyle(fontSize: 16),
|
||||
/// dayOfWeekStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||
/// inactiveDateStyle: TextStyle(color: Colors.grey),
|
||||
/// selectedDateStyle: TextStyle(color: Colors.red),
|
||||
/// titleStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
/// rowHeight: 40,
|
||||
/// locale: 'en_US',
|
||||
/// )
|
||||
/// ```
|
||||
class FlutterFlowCalendar extends StatefulWidget {
|
||||
/// Creates a new instance of [FlutterFlowCalendar].
|
||||
///
|
||||
/// - `color` parameter specifies the background color of the calendar.
|
||||
/// - `onChange` parameter is a callback function that will be called when the selected date range changes.
|
||||
/// - `initialDate` parameter specifies the initial date to be displayed on the calendar.
|
||||
/// - `weekFormat` parameter determines whether the calendar should be displayed in week format.
|
||||
/// - `weekStartsMonday` parameter determines whether the week starts on Monday.
|
||||
/// - `twoRowHeader` parameter determines whether the header should be displayed in two rows.
|
||||
/// - `iconColor` parameter specifies the color of the icons used in the calendar.
|
||||
/// - `dateStyle` parameter specifies the text style for the dates.
|
||||
/// - `dayOfWeekStyle` parameter specifies the text style for the day of the week labels.
|
||||
/// - `inactiveDateStyle` parameter specifies the text style for inactive dates.
|
||||
/// - `selectedDateStyle` parameter specifies the text style for selected dates.
|
||||
/// - `titleStyle` parameter specifies the text style for the calendar title.
|
||||
/// - `rowHeight` parameter specifies the height of each row in the calendar.
|
||||
/// - `locale` parameter specifies the locale to be used for formatting dates.
|
||||
const FlutterFlowCalendar({
|
||||
super.key,
|
||||
required this.color,
|
||||
this.onChange,
|
||||
this.initialDate,
|
||||
this.weekFormat = false,
|
||||
this.weekStartsMonday = false,
|
||||
this.twoRowHeader = false,
|
||||
this.iconColor,
|
||||
this.dateStyle,
|
||||
this.dayOfWeekStyle,
|
||||
this.inactiveDateStyle,
|
||||
this.selectedDateStyle,
|
||||
this.titleStyle,
|
||||
this.rowHeight,
|
||||
this.locale,
|
||||
});
|
||||
|
||||
/// Determines whether the calendar should be displayed in week format.
|
||||
final bool weekFormat;
|
||||
|
||||
/// Determines whether the week starts on Monday.
|
||||
final bool weekStartsMonday;
|
||||
|
||||
/// Determines whether the header should have two rows.
|
||||
final bool twoRowHeader;
|
||||
|
||||
/// The color of the calendar.
|
||||
final Color color;
|
||||
|
||||
/// A callback function that is called when the selected date range changes.
|
||||
final void Function(DateTimeRange?)? onChange;
|
||||
|
||||
/// The initial date to be displayed on the calendar.
|
||||
final DateTime? initialDate;
|
||||
|
||||
/// The color of the icons in the calendar.
|
||||
final Color? iconColor;
|
||||
|
||||
/// The text style for the dates displayed on the calendar.
|
||||
final TextStyle? dateStyle;
|
||||
|
||||
/// The text style for the day of the week displayed on the calendar.
|
||||
final TextStyle? dayOfWeekStyle;
|
||||
|
||||
/// The text style for the inactive dates on the calendar.
|
||||
final TextStyle? inactiveDateStyle;
|
||||
|
||||
/// The text style for the selected dates on the calendar.
|
||||
final TextStyle? selectedDateStyle;
|
||||
|
||||
/// The text style for the title of the calendar.
|
||||
final TextStyle? titleStyle;
|
||||
|
||||
/// The height of each row in the calendar.
|
||||
final double? rowHeight;
|
||||
|
||||
/// The locale to be used for formatting dates.
|
||||
final String? locale;
|
||||
|
||||
@override
|
||||
State<FlutterFlowCalendar> createState() => _FlutterFlowCalendarState();
|
||||
}
|
||||
|
||||
class _FlutterFlowCalendarState extends State<FlutterFlowCalendar> {
|
||||
late DateTime focusedDay;
|
||||
late DateTime selectedDay;
|
||||
late DateTimeRange selectedRange;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
focusedDay = widget.initialDate ?? DateTime.now();
|
||||
selectedDay = widget.initialDate ?? DateTime.now();
|
||||
selectedRange = DateTimeRange(
|
||||
start: selectedDay.startOfDay,
|
||||
end: selectedDay.endOfDay,
|
||||
);
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((_) => setSelectedDay(selectedRange.start));
|
||||
}
|
||||
|
||||
CalendarFormat get calendarFormat =>
|
||||
widget.weekFormat ? CalendarFormat.week : CalendarFormat.month;
|
||||
|
||||
StartingDayOfWeek get startingDayOfWeek => widget.weekStartsMonday
|
||||
? StartingDayOfWeek.monday
|
||||
: StartingDayOfWeek.sunday;
|
||||
|
||||
Color get color => widget.color;
|
||||
|
||||
Color get lightColor => widget.color.withOpacity(0.85);
|
||||
|
||||
Color get lighterColor => widget.color.withOpacity(0.60);
|
||||
|
||||
void setSelectedDay(
|
||||
DateTime? newSelectedDay, [
|
||||
DateTime? newSelectedEnd,
|
||||
]) {
|
||||
final newRange = newSelectedDay == null
|
||||
? null
|
||||
: DateTimeRange(
|
||||
start: newSelectedDay.startOfDay,
|
||||
end: newSelectedEnd ?? newSelectedDay.endOfDay,
|
||||
);
|
||||
setState(() {
|
||||
selectedDay = newSelectedDay ?? selectedDay;
|
||||
selectedRange = newRange ?? selectedRange;
|
||||
if (widget.onChange != null) {
|
||||
widget.onChange!(newRange);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CalendarHeader(
|
||||
focusedDay: focusedDay,
|
||||
onLeftChevronTap: () => setState(
|
||||
() => focusedDay = widget.weekFormat
|
||||
? _previousWeek(focusedDay)
|
||||
: _previousMonth(focusedDay),
|
||||
),
|
||||
onRightChevronTap: () => setState(
|
||||
() => focusedDay = widget.weekFormat
|
||||
? _nextWeek(focusedDay)
|
||||
: _nextMonth(focusedDay),
|
||||
),
|
||||
onTodayButtonTap: () => setState(() => focusedDay = DateTime.now()),
|
||||
titleStyle: widget.titleStyle,
|
||||
iconColor: widget.iconColor,
|
||||
locale: widget.locale,
|
||||
twoRowHeader: widget.twoRowHeader,
|
||||
),
|
||||
TableCalendar(
|
||||
focusedDay: focusedDay,
|
||||
selectedDayPredicate: (date) => isSameDay(selectedDay, date),
|
||||
firstDay: kFirstDay,
|
||||
lastDay: kLastDay,
|
||||
calendarFormat: calendarFormat,
|
||||
headerVisible: false,
|
||||
locale: widget.locale,
|
||||
rowHeight: widget.rowHeight ?? MediaQuery.sizeOf(context).width / 7,
|
||||
calendarStyle: CalendarStyle(
|
||||
defaultTextStyle:
|
||||
widget.dateStyle ?? const TextStyle(color: Color(0xFF5A5A5A)),
|
||||
weekendTextStyle: widget.dateStyle ??
|
||||
const TextStyle(color: Color(0xFF5A5A5A)),
|
||||
holidayTextStyle: widget.dateStyle ??
|
||||
const TextStyle(color: Color(0xFF5C6BC0)),
|
||||
selectedTextStyle:
|
||||
const TextStyle(color: Color(0xFFFAFAFA), fontSize: 16.0)
|
||||
.merge(widget.selectedDateStyle),
|
||||
todayTextStyle:
|
||||
const TextStyle(color: Color(0xFFFAFAFA), fontSize: 16.0)
|
||||
.merge(widget.selectedDateStyle),
|
||||
outsideTextStyle: const TextStyle(color: Color(0xFF9E9E9E))
|
||||
.merge(widget.inactiveDateStyle),
|
||||
selectedDecoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
todayDecoration: BoxDecoration(
|
||||
color: lighterColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
markerDecoration: BoxDecoration(
|
||||
color: lightColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
markersMaxCount: 3,
|
||||
canMarkersOverflow: true,
|
||||
),
|
||||
availableGestures: AvailableGestures.horizontalSwipe,
|
||||
startingDayOfWeek: startingDayOfWeek,
|
||||
daysOfWeekStyle: DaysOfWeekStyle(
|
||||
weekdayStyle: const TextStyle(color: Color(0xFF616161))
|
||||
.merge(widget.dayOfWeekStyle),
|
||||
weekendStyle: const TextStyle(color: Color(0xFF616161))
|
||||
.merge(widget.dayOfWeekStyle),
|
||||
),
|
||||
onPageChanged: (focused) {
|
||||
if (focusedDay.startOfDay != focused.startOfDay) {
|
||||
setState(() => focusedDay = focused);
|
||||
}
|
||||
},
|
||||
onDaySelected: (newSelectedDay, focused) {
|
||||
if (!isSameDay(selectedDay, newSelectedDay)) {
|
||||
setSelectedDay(newSelectedDay);
|
||||
if (focusedDay.startOfDay != focused.startOfDay) {
|
||||
setState(() => focusedDay = focused);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class CalendarHeader extends StatelessWidget {
|
||||
const CalendarHeader({
|
||||
super.key,
|
||||
required this.focusedDay,
|
||||
required this.onLeftChevronTap,
|
||||
required this.onRightChevronTap,
|
||||
required this.onTodayButtonTap,
|
||||
this.iconColor,
|
||||
this.titleStyle,
|
||||
this.locale,
|
||||
this.twoRowHeader = false,
|
||||
});
|
||||
|
||||
final DateTime focusedDay;
|
||||
final VoidCallback onLeftChevronTap;
|
||||
final VoidCallback onRightChevronTap;
|
||||
final VoidCallback onTodayButtonTap;
|
||||
final Color? iconColor;
|
||||
final TextStyle? titleStyle;
|
||||
final String? locale;
|
||||
final bool twoRowHeader;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: const BoxDecoration(),
|
||||
margin: const EdgeInsets.all(0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: twoRowHeader ? _buildTwoRowHeader() : _buildOneRowHeader(),
|
||||
);
|
||||
|
||||
Widget _buildTwoRowHeader() => Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
_buildDateWidget(),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: _buildCustomIconButtons(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildOneRowHeader() => Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
const SizedBox(width: 16),
|
||||
_buildDateWidget(),
|
||||
..._buildCustomIconButtons(),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildDateWidget() => Expanded(
|
||||
child: Text(
|
||||
DateFormat.yMMMM(locale).format(focusedDay),
|
||||
style: const TextStyle(fontSize: 17).merge(titleStyle),
|
||||
),
|
||||
);
|
||||
|
||||
List<Widget> _buildCustomIconButtons() => <Widget>[
|
||||
CustomIconButton(
|
||||
icon: Icon(Icons.calendar_today, color: iconColor),
|
||||
onTap: onTodayButtonTap,
|
||||
),
|
||||
CustomIconButton(
|
||||
icon: Icon(Icons.chevron_left, color: iconColor),
|
||||
onTap: onLeftChevronTap,
|
||||
),
|
||||
CustomIconButton(
|
||||
icon: Icon(Icons.chevron_right, color: iconColor),
|
||||
onTap: onRightChevronTap,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
class CustomIconButton extends StatelessWidget {
|
||||
const CustomIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
this.margin = const EdgeInsets.symmetric(horizontal: 4),
|
||||
this.padding = const EdgeInsets.all(10),
|
||||
});
|
||||
|
||||
final Icon icon;
|
||||
final VoidCallback onTap;
|
||||
final EdgeInsetsGeometry margin;
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: margin,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Icon(
|
||||
icon.icon,
|
||||
color: icon.color,
|
||||
size: icon.size,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _previousWeek(DateTime week) {
|
||||
return week.subtract(const Duration(days: 7));
|
||||
}
|
||||
|
||||
DateTime _nextWeek(DateTime week) {
|
||||
return week.add(const Duration(days: 7));
|
||||
}
|
||||
|
||||
DateTime _previousMonth(DateTime month) {
|
||||
if (month.month == 1) {
|
||||
return DateTime(month.year - 1, 12);
|
||||
} else {
|
||||
return DateTime(month.year, month.month - 1);
|
||||
}
|
||||
}
|
||||
|
||||
DateTime _nextMonth(DateTime month) {
|
||||
if (month.month == 12) {
|
||||
return DateTime(month.year + 1, 1);
|
||||
} else {
|
||||
return DateTime(month.year, month.month + 1);
|
||||
}
|
||||
}
|
||||
586
lib/src/widgets/flutter_flow_charts.dart
Normal file
586
lib/src/widgets/flutter_flow_charts.dart
Normal file
@@ -0,0 +1,586 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
export 'package:fl_chart/fl_chart.dart'
|
||||
show BarAreaData, FlDotData, LineChartBarData, BarChartAlignment;
|
||||
|
||||
/// A line chart widget that displays a line chart with customizable data and styling.
|
||||
///
|
||||
/// The [FlutterFlowLineChart] widget is used to display a line chart in a Flutter application.
|
||||
class FlutterFlowLineChart extends StatelessWidget {
|
||||
const FlutterFlowLineChart({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.xAxisLabelInfo,
|
||||
required this.yAxisLabelInfo,
|
||||
required this.axisBounds,
|
||||
this.chartStylingInfo = const ChartStylingInfo(),
|
||||
});
|
||||
|
||||
/// The data to be displayed in the line chart.
|
||||
final List<FFLineChartData> data;
|
||||
|
||||
/// The information for labeling the x-axis.
|
||||
final AxisLabelInfo xAxisLabelInfo;
|
||||
|
||||
/// The information for labeling the y-axis.
|
||||
final AxisLabelInfo yAxisLabelInfo;
|
||||
|
||||
/// The bounds for the chart's axes.
|
||||
final AxisBounds axisBounds;
|
||||
|
||||
/// The styling information for the chart.
|
||||
final ChartStylingInfo chartStylingInfo;
|
||||
|
||||
List<LineChartBarData> get dataWithSpots =>
|
||||
data.map((d) => d.settings.copyWith(spots: d.spots)).toList();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => LineChart(
|
||||
LineChartData(
|
||||
lineTouchData: LineTouchData(
|
||||
handleBuiltInTouches: chartStylingInfo.enableTooltip,
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipColor: (group) =>
|
||||
chartStylingInfo.tooltipBackgroundColor ?? Colors.black,
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(show: chartStylingInfo.showGrid),
|
||||
borderData: FlBorderData(
|
||||
border: Border.all(
|
||||
color: chartStylingInfo.borderColor,
|
||||
width: chartStylingInfo.borderWidth,
|
||||
),
|
||||
show: chartStylingInfo.showBorder,
|
||||
),
|
||||
titlesData: getTitlesData(
|
||||
xAxisLabelInfo,
|
||||
yAxisLabelInfo,
|
||||
),
|
||||
lineBarsData: dataWithSpots,
|
||||
minX: axisBounds.minX,
|
||||
minY: axisBounds.minY,
|
||||
maxX: axisBounds.maxX,
|
||||
maxY: axisBounds.maxY,
|
||||
backgroundColor: chartStylingInfo.backgroundColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// A bar chart widget that displays data in a bar format.
|
||||
///
|
||||
/// The [FlutterFlowBarChart] widget is used to create a bar chart in FlutterFlow.
|
||||
class FlutterFlowBarChart extends StatelessWidget {
|
||||
const FlutterFlowBarChart({
|
||||
super.key,
|
||||
required this.barData,
|
||||
required this.xLabels,
|
||||
required this.xAxisLabelInfo,
|
||||
required this.yAxisLabelInfo,
|
||||
required this.axisBounds,
|
||||
this.stacked = false,
|
||||
this.barWidth,
|
||||
this.barBorderRadius,
|
||||
this.barSpace,
|
||||
this.groupSpace,
|
||||
this.alignment = BarChartAlignment.center,
|
||||
this.chartStylingInfo = const ChartStylingInfo(),
|
||||
});
|
||||
|
||||
/// The data for the bar chart.
|
||||
final List<FFBarChartData> barData;
|
||||
|
||||
/// The labels for the x-axis of the bar chart.
|
||||
final List<String> xLabels;
|
||||
|
||||
/// The information about the labels for the x-axis.
|
||||
final AxisLabelInfo xAxisLabelInfo;
|
||||
|
||||
/// The information about the labels for the y-axis.
|
||||
final AxisLabelInfo yAxisLabelInfo;
|
||||
|
||||
/// The bounds for the x and y axes.
|
||||
final AxisBounds axisBounds;
|
||||
|
||||
/// Determines whether the bars in the chart are stacked.
|
||||
final bool stacked;
|
||||
|
||||
/// The width of each bar in the chart.
|
||||
final double? barWidth;
|
||||
|
||||
/// The border radius of each bar in the chart.
|
||||
final BorderRadius? barBorderRadius;
|
||||
|
||||
/// The space between each bar in the chart.
|
||||
final double? barSpace;
|
||||
|
||||
/// The space between each group of bars in the chart.
|
||||
final double? groupSpace;
|
||||
|
||||
/// The alignment of the bars within the chart.
|
||||
final BarChartAlignment alignment;
|
||||
|
||||
/// The styling information for the chart.
|
||||
final ChartStylingInfo chartStylingInfo;
|
||||
|
||||
Map<int, List<double>> get dataMap => xLabels.asMap().map((key, value) =>
|
||||
MapEntry(key, barData.map((data) => data.data[key]).toList()));
|
||||
|
||||
List<BarChartGroupData> get groups => dataMap.entries.map((entry) {
|
||||
final groupInt = entry.key;
|
||||
final groupData = entry.value;
|
||||
return BarChartGroupData(
|
||||
x: groupInt,
|
||||
barsSpace: barSpace,
|
||||
barRods: groupData.asMap().entries.map((rod) {
|
||||
final rodInt = rod.key;
|
||||
final rodSettings = barData[rodInt];
|
||||
final rodValue = rod.value;
|
||||
return BarChartRodData(
|
||||
toY: rodValue,
|
||||
color: rodSettings.color,
|
||||
width: barWidth,
|
||||
borderRadius: barBorderRadius,
|
||||
borderSide: BorderSide(
|
||||
width: rodSettings.borderWidth,
|
||||
color: rodSettings.borderColor,
|
||||
),
|
||||
);
|
||||
}).toList());
|
||||
}).toList();
|
||||
|
||||
List<BarChartGroupData> get stacks => dataMap.entries.map((entry) {
|
||||
final groupInt = entry.key;
|
||||
final stackData = entry.value;
|
||||
return BarChartGroupData(
|
||||
x: groupInt,
|
||||
barsSpace: barSpace,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: sum(stackData),
|
||||
width: barWidth,
|
||||
borderRadius: barBorderRadius,
|
||||
rodStackItems: stackData.asMap().entries.map((stack) {
|
||||
final stackInt = stack.key;
|
||||
final stackSettings = barData[stackInt];
|
||||
final start =
|
||||
stackInt == 0 ? 0.0 : sum(stackData.sublist(0, stackInt));
|
||||
return BarChartRodStackItem(
|
||||
start,
|
||||
start + stack.value,
|
||||
stackSettings.color,
|
||||
BorderSide(
|
||||
width: stackSettings.borderWidth,
|
||||
color: stackSettings.borderColor,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
],
|
||||
);
|
||||
}).toList();
|
||||
|
||||
double sum(List<double> list) => list.reduce((a, b) => a + b);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
barTouchData: BarTouchData(
|
||||
handleBuiltInTouches: chartStylingInfo.enableTooltip,
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipColor: (group) =>
|
||||
chartStylingInfo.tooltipBackgroundColor ?? Colors.black,
|
||||
),
|
||||
),
|
||||
alignment: alignment,
|
||||
gridData: FlGridData(show: chartStylingInfo.showGrid),
|
||||
borderData: FlBorderData(
|
||||
border: Border.all(
|
||||
color: chartStylingInfo.borderColor,
|
||||
width: chartStylingInfo.borderWidth,
|
||||
),
|
||||
show: chartStylingInfo.showBorder,
|
||||
),
|
||||
titlesData: getTitlesData(
|
||||
xAxisLabelInfo,
|
||||
yAxisLabelInfo,
|
||||
getXTitlesWidget: (val, _) => Text(
|
||||
xLabels[val.toInt()],
|
||||
style: xAxisLabelInfo.labelTextStyle,
|
||||
),
|
||||
),
|
||||
barGroups: stacked ? stacks : groups,
|
||||
groupsSpace: groupSpace,
|
||||
minY: axisBounds.minY,
|
||||
maxY: axisBounds.maxY,
|
||||
backgroundColor: chartStylingInfo.backgroundColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum PieChartSectionLabelType {
|
||||
none,
|
||||
value,
|
||||
percent,
|
||||
}
|
||||
|
||||
/// A widget that displays a pie chart using the provided data.
|
||||
class FlutterFlowPieChart extends StatelessWidget {
|
||||
const FlutterFlowPieChart({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.donutHoleRadius = 0,
|
||||
this.donutHoleColor = Colors.transparent,
|
||||
this.sectionLabelType = PieChartSectionLabelType.none,
|
||||
this.sectionLabelStyle,
|
||||
this.labelFormatter = const LabelFormatter(),
|
||||
});
|
||||
|
||||
final FFPieChartData data;
|
||||
final double donutHoleRadius;
|
||||
final Color donutHoleColor;
|
||||
final PieChartSectionLabelType sectionLabelType;
|
||||
final TextStyle? sectionLabelStyle;
|
||||
final LabelFormatter labelFormatter;
|
||||
|
||||
double get sumOfValues => data.data.reduce((a, b) => a + b);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PieChart(
|
||||
PieChartData(
|
||||
centerSpaceRadius: donutHoleRadius,
|
||||
centerSpaceColor: donutHoleColor,
|
||||
sectionsSpace: 0,
|
||||
sections: data.data.asMap().entries.map(
|
||||
(section) {
|
||||
String? title;
|
||||
final index = section.key;
|
||||
final sectionData = section.value;
|
||||
final colorsLength = data.colors.length;
|
||||
final otherPropsLength = data.radius.length;
|
||||
switch (sectionLabelType) {
|
||||
case PieChartSectionLabelType.value:
|
||||
title = formatLabel(labelFormatter, sectionData);
|
||||
break;
|
||||
case PieChartSectionLabelType.percent:
|
||||
title =
|
||||
'\${formatLabel(labelFormatter, sectionData / sumOfValues * 100)}%';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return PieChartSectionData(
|
||||
value: sectionData,
|
||||
color: data.colors[index % colorsLength],
|
||||
radius: otherPropsLength == 1
|
||||
? data.radius.first
|
||||
: data.radius[index],
|
||||
borderSide: BorderSide(
|
||||
color: (otherPropsLength == 1
|
||||
? data.borderColor?.first
|
||||
: data.borderColor?.elementAt(index)) ??
|
||||
Colors.transparent,
|
||||
width: (otherPropsLength == 1
|
||||
? data.borderWidth?.first
|
||||
: data.borderWidth?.elementAt(index)) ??
|
||||
0.0,
|
||||
),
|
||||
showTitle: sectionLabelType != PieChartSectionLabelType.none,
|
||||
titleStyle: sectionLabelStyle,
|
||||
title: title,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class FlutterFlowChartLegendWidget extends StatelessWidget {
|
||||
const FlutterFlowChartLegendWidget({
|
||||
super.key,
|
||||
required this.entries,
|
||||
this.width,
|
||||
this.height,
|
||||
this.textStyle,
|
||||
this.padding,
|
||||
this.backgroundColor = Colors.transparent,
|
||||
this.borderRadius,
|
||||
this.borderWidth = 1.0,
|
||||
this.borderColor = const Color(0xFF000000),
|
||||
this.indicatorSize = 10,
|
||||
this.indicatorBorderRadius,
|
||||
this.textPadding = const EdgeInsets.all(0),
|
||||
});
|
||||
|
||||
final List<LegendEntry> entries;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final TextStyle? textStyle;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final Color backgroundColor;
|
||||
final BorderRadius? borderRadius;
|
||||
final double borderWidth;
|
||||
final Color borderColor;
|
||||
final double indicatorSize;
|
||||
final BorderRadius? indicatorBorderRadius;
|
||||
final EdgeInsetsGeometry textPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: width,
|
||||
height: height,
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: borderRadius,
|
||||
border: Border.all(
|
||||
color: borderColor,
|
||||
width: borderWidth,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: entries
|
||||
.map(
|
||||
(entry) => Row(
|
||||
children: [
|
||||
Container(
|
||||
height: indicatorSize,
|
||||
width: indicatorSize,
|
||||
decoration: BoxDecoration(
|
||||
color: entry.color,
|
||||
borderRadius: indicatorBorderRadius,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: textPadding,
|
||||
child: Text(
|
||||
entry.name,
|
||||
style: textStyle,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class LegendEntry {
|
||||
const LegendEntry(this.color, this.name);
|
||||
|
||||
final Color color;
|
||||
final String name;
|
||||
}
|
||||
|
||||
class ChartStylingInfo {
|
||||
const ChartStylingInfo({
|
||||
this.backgroundColor = Colors.white,
|
||||
this.showGrid = false,
|
||||
this.enableTooltip = false,
|
||||
this.tooltipBackgroundColor,
|
||||
this.borderColor = Colors.black,
|
||||
this.borderWidth = 1.0,
|
||||
this.showBorder = true,
|
||||
});
|
||||
|
||||
final Color backgroundColor;
|
||||
final bool showGrid;
|
||||
final bool enableTooltip;
|
||||
final Color? tooltipBackgroundColor;
|
||||
final Color borderColor;
|
||||
final double borderWidth;
|
||||
final bool showBorder;
|
||||
}
|
||||
|
||||
class AxisLabelInfo {
|
||||
const AxisLabelInfo({
|
||||
this.title = '',
|
||||
this.titleTextStyle,
|
||||
this.showLabels = false,
|
||||
this.labelTextStyle,
|
||||
this.labelInterval,
|
||||
this.labelFormatter = const LabelFormatter(),
|
||||
this.reservedSize,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final TextStyle? titleTextStyle;
|
||||
final bool showLabels;
|
||||
final TextStyle? labelTextStyle;
|
||||
final double? labelInterval;
|
||||
final LabelFormatter labelFormatter;
|
||||
final double? reservedSize;
|
||||
}
|
||||
|
||||
class LabelFormatter {
|
||||
const LabelFormatter({
|
||||
this.numberFormat,
|
||||
});
|
||||
|
||||
final String Function(double)? numberFormat;
|
||||
NumberFormat get defaultFormat => NumberFormat()..significantDigits = 2;
|
||||
}
|
||||
|
||||
class AxisBounds {
|
||||
const AxisBounds({this.minX, this.minY, this.maxX, this.maxY});
|
||||
|
||||
final double? minX;
|
||||
final double? minY;
|
||||
final double? maxX;
|
||||
final double? maxY;
|
||||
}
|
||||
|
||||
class FFLineChartData {
|
||||
const FFLineChartData({
|
||||
required this.xData,
|
||||
required this.yData,
|
||||
required this.settings,
|
||||
});
|
||||
|
||||
final List<dynamic> xData;
|
||||
final List<dynamic> yData;
|
||||
final LineChartBarData settings;
|
||||
|
||||
List<FlSpot> get spots {
|
||||
final x = _dataToDouble(xData);
|
||||
final y = _dataToDouble(yData);
|
||||
assert(x.length == y.length, 'X and Y data must be the same length');
|
||||
|
||||
return Iterable<int>.generate(min(x.length, y.length))
|
||||
.where((i) => x[i] != null && y[i] != null)
|
||||
.map((i) => FlSpot(x[i]!, y[i]!))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
class FFBarChartData {
|
||||
const FFBarChartData({
|
||||
required this.yData,
|
||||
required this.color,
|
||||
this.borderWidth = 0,
|
||||
this.borderColor = Colors.transparent,
|
||||
});
|
||||
|
||||
final List<dynamic> yData;
|
||||
final Color color;
|
||||
final double borderWidth;
|
||||
final Color borderColor;
|
||||
|
||||
List<double> get data => _dataToDouble(yData).map((e) => e ?? 0.0).toList();
|
||||
}
|
||||
|
||||
class FFPieChartData {
|
||||
const FFPieChartData({
|
||||
required this.values,
|
||||
required this.colors,
|
||||
required this.radius,
|
||||
this.borderWidth,
|
||||
this.borderColor,
|
||||
});
|
||||
|
||||
final List<dynamic> values;
|
||||
final List<Color> colors;
|
||||
final List<double> radius;
|
||||
final List<double>? borderWidth;
|
||||
final List<Color>? borderColor;
|
||||
|
||||
List<double> get data => _dataToDouble(values).map((e) => e ?? 0.0).toList();
|
||||
}
|
||||
|
||||
List<double?> _dataToDouble(List<dynamic> data) {
|
||||
if (data.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
if (data.first is double) {
|
||||
return data.map((d) => d as double).toList();
|
||||
}
|
||||
if (data.first is int) {
|
||||
return data.map((d) => (d as int).toDouble()).toList();
|
||||
}
|
||||
if (data.first is DateTime) {
|
||||
return data
|
||||
.map((d) => (d as DateTime).millisecondsSinceEpoch.toDouble())
|
||||
.toList();
|
||||
}
|
||||
if (data.first is String) {
|
||||
// First try to parse as doubles
|
||||
if (double.tryParse(data.first as String) != null) {
|
||||
return data.map((d) => double.tryParse(d as String)).toList();
|
||||
}
|
||||
if (int.tryParse(data.first as String) != null) {
|
||||
return data.map((d) => int.tryParse(d as String)?.toDouble()).toList();
|
||||
}
|
||||
if (DateTime.tryParse(data.first as String) != null) {
|
||||
return data
|
||||
.map((d) =>
|
||||
DateTime.tryParse(d as String)?.millisecondsSinceEpoch.toDouble())
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
FlTitlesData getTitlesData(
|
||||
AxisLabelInfo xAxisLabelInfo,
|
||||
AxisLabelInfo yAxisLabelInfo, {
|
||||
Widget Function(double, TitleMeta)? getXTitlesWidget,
|
||||
}) =>
|
||||
FlTitlesData(
|
||||
bottomTitles: AxisTitles(
|
||||
axisNameWidget: xAxisLabelInfo.title.isEmpty
|
||||
? null
|
||||
: Text(
|
||||
xAxisLabelInfo.title,
|
||||
style: xAxisLabelInfo.titleTextStyle,
|
||||
),
|
||||
axisNameSize: xAxisLabelInfo.titleTextStyle?.fontSize != null
|
||||
? xAxisLabelInfo.titleTextStyle!.fontSize! + 12
|
||||
: 16,
|
||||
sideTitles: SideTitles(
|
||||
getTitlesWidget: (val, meta) => getXTitlesWidget != null
|
||||
? getXTitlesWidget(val, meta)
|
||||
: Text(
|
||||
formatLabel(xAxisLabelInfo.labelFormatter, val),
|
||||
style: xAxisLabelInfo.labelTextStyle,
|
||||
),
|
||||
showTitles: xAxisLabelInfo.showLabels,
|
||||
interval: xAxisLabelInfo.labelInterval,
|
||||
reservedSize: xAxisLabelInfo.reservedSize ?? 22,
|
||||
),
|
||||
),
|
||||
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: AxisTitles(
|
||||
axisNameWidget: yAxisLabelInfo.title.isEmpty
|
||||
? null
|
||||
: Text(
|
||||
yAxisLabelInfo.title,
|
||||
style: yAxisLabelInfo.titleTextStyle,
|
||||
),
|
||||
axisNameSize: yAxisLabelInfo.titleTextStyle?.fontSize != null
|
||||
? yAxisLabelInfo.titleTextStyle!.fontSize! + 12
|
||||
: 16,
|
||||
sideTitles: SideTitles(
|
||||
getTitlesWidget: (val, _) => Text(
|
||||
formatLabel(yAxisLabelInfo.labelFormatter, val),
|
||||
style: yAxisLabelInfo.labelTextStyle,
|
||||
),
|
||||
showTitles: yAxisLabelInfo.showLabels,
|
||||
interval: yAxisLabelInfo.labelInterval,
|
||||
reservedSize: yAxisLabelInfo.reservedSize ?? 22,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
String formatLabel(LabelFormatter formatter, double value) {
|
||||
if (formatter.numberFormat != null) {
|
||||
return formatter.numberFormat!(value);
|
||||
}
|
||||
return formatter.defaultFormat.format(value);
|
||||
}
|
||||
148
lib/src/widgets/flutter_flow_checkbox_group.dart
Normal file
148
lib/src/widgets/flutter_flow_checkbox_group.dart
Normal file
@@ -0,0 +1,148 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutterflow_ui/src/utils/form_field_controller.dart';
|
||||
|
||||
/// A group of checkboxes that allows the user to select multiple options.
|
||||
class FlutterFlowCheckboxGroup extends StatefulWidget {
|
||||
/// Creates a [FlutterFlowCheckboxGroup].
|
||||
///
|
||||
/// - [options] parameter is a list of strings representing the available options.
|
||||
/// - [onChanged] parameter is a callback function that is called when the selection changes.
|
||||
/// - [controller] parameter is a controller for the form field that holds the selected options.
|
||||
/// - [textStyle] parameter is the style of the text for the checkboxes.
|
||||
/// - [labelPadding] parameter is the padding around the checkbox labels.
|
||||
/// - [itemPadding] parameter is the padding around each checkbox item.
|
||||
/// - [activeColor] parameter is the color of the checkbox when it is selected.
|
||||
/// - [checkColor] parameter is the color of the check mark inside the checkbox.
|
||||
/// - [checkboxBorderRadius] parameter is the border radius of the checkbox.
|
||||
/// - [checkboxBorderColor] parameter is the color of the checkbox border.
|
||||
/// - [initialized] parameter indicates whether the checkbox group is initialized with a value.
|
||||
/// - [unselectedTextStyle] parameter is the style of the text for unselected checkboxes.
|
||||
const FlutterFlowCheckboxGroup({
|
||||
super.key,
|
||||
required this.options,
|
||||
required this.onChanged,
|
||||
required this.controller,
|
||||
required this.textStyle,
|
||||
this.labelPadding,
|
||||
this.itemPadding,
|
||||
required this.activeColor,
|
||||
required this.checkColor,
|
||||
this.checkboxBorderRadius,
|
||||
required this.checkboxBorderColor,
|
||||
this.initialized = true,
|
||||
this.unselectedTextStyle,
|
||||
});
|
||||
|
||||
final List<String> options;
|
||||
final void Function(List<String>)? onChanged;
|
||||
final FormFieldController<List<String>> controller;
|
||||
final TextStyle textStyle;
|
||||
final EdgeInsetsGeometry? labelPadding;
|
||||
final EdgeInsetsGeometry? itemPadding;
|
||||
final Color activeColor;
|
||||
final Color checkColor;
|
||||
final BorderRadius? checkboxBorderRadius;
|
||||
final Color checkboxBorderColor;
|
||||
final bool initialized;
|
||||
final TextStyle? unselectedTextStyle;
|
||||
|
||||
@override
|
||||
State<FlutterFlowCheckboxGroup> createState() =>
|
||||
_FlutterFlowCheckboxGroupState();
|
||||
}
|
||||
|
||||
class _FlutterFlowCheckboxGroupState extends State<FlutterFlowCheckboxGroup> {
|
||||
late List<String> checkboxValues;
|
||||
late void Function() _selectedValueListener;
|
||||
ValueListenable<List<String>?> get changeSelectedValues => widget.controller;
|
||||
List<String> get selectedValues => widget.controller.value ?? [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
checkboxValues = List.from(widget.controller.initialValue ?? []);
|
||||
if (!widget.initialized && checkboxValues.isNotEmpty) {
|
||||
SchedulerBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(checkboxValues);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
_selectedValueListener = () {
|
||||
if (!listEquals(checkboxValues, selectedValues)) {
|
||||
setState(() => checkboxValues = List.from(selectedValues));
|
||||
}
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(selectedValues);
|
||||
}
|
||||
};
|
||||
changeSelectedValues.addListener(_selectedValueListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
changeSelectedValues.removeListener(_selectedValueListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: widget.options.length,
|
||||
itemBuilder: (context, index) {
|
||||
final option = widget.options[index];
|
||||
final selected = selectedValues.contains(option);
|
||||
final unselectedTextStyle =
|
||||
widget.unselectedTextStyle ?? widget.textStyle;
|
||||
return Theme(
|
||||
data: ThemeData(unselectedWidgetColor: widget.checkboxBorderColor),
|
||||
child: Padding(
|
||||
padding: widget.itemPadding ?? EdgeInsets.zero,
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: selected,
|
||||
onChanged: widget.onChanged != null
|
||||
? (isSelected) {
|
||||
if (isSelected == null) {
|
||||
return;
|
||||
}
|
||||
isSelected
|
||||
? checkboxValues.add(option)
|
||||
: checkboxValues.remove(option);
|
||||
widget.controller.value = List.from(checkboxValues);
|
||||
setState(() {});
|
||||
}
|
||||
: null,
|
||||
activeColor: widget.activeColor,
|
||||
checkColor: widget.checkColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
widget.checkboxBorderRadius ?? BorderRadius.zero,
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: widget.labelPadding ?? EdgeInsets.zero,
|
||||
child: Text(
|
||||
widget.options[index],
|
||||
style:
|
||||
selected ? widget.textStyle : unselectedTextStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
176
lib/src/widgets/flutter_flow_choice_chips.dart
Normal file
176
lib/src/widgets/flutter_flow_choice_chips.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutterflow_ui/src/utils/flutter_flow_helpers.dart';
|
||||
import 'package:flutterflow_ui/src/utils/form_field_controller.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
class ChipData {
|
||||
const ChipData(this.label, [this.iconData]);
|
||||
final String label;
|
||||
final IconData? iconData;
|
||||
}
|
||||
|
||||
class ChipStyle {
|
||||
const ChipStyle({
|
||||
this.backgroundColor,
|
||||
this.textStyle,
|
||||
this.iconColor,
|
||||
this.iconSize,
|
||||
this.labelPadding,
|
||||
this.elevation,
|
||||
this.borderColor,
|
||||
this.borderWidth,
|
||||
this.borderRadius,
|
||||
});
|
||||
final Color? backgroundColor;
|
||||
final TextStyle? textStyle;
|
||||
final Color? iconColor;
|
||||
final double? iconSize;
|
||||
final EdgeInsetsGeometry? labelPadding;
|
||||
final double? elevation;
|
||||
final Color? borderColor;
|
||||
final double? borderWidth;
|
||||
final BorderRadius? borderRadius;
|
||||
}
|
||||
|
||||
class FlutterFlowChoiceChips extends StatefulWidget {
|
||||
const FlutterFlowChoiceChips({
|
||||
super.key,
|
||||
required this.options,
|
||||
required this.onChanged,
|
||||
required this.controller,
|
||||
required this.selectedChipStyle,
|
||||
required this.unselectedChipStyle,
|
||||
required this.chipSpacing,
|
||||
this.rowSpacing = 0.0,
|
||||
required this.multiselect,
|
||||
this.initialized = true,
|
||||
this.alignment = WrapAlignment.start,
|
||||
this.disabledColor,
|
||||
this.wrapped = true,
|
||||
});
|
||||
|
||||
final List<ChipData> options;
|
||||
final void Function(List<String>?)? onChanged;
|
||||
final FormFieldController<List<String>> controller;
|
||||
final ChipStyle selectedChipStyle;
|
||||
final ChipStyle unselectedChipStyle;
|
||||
final double chipSpacing;
|
||||
final double rowSpacing;
|
||||
final bool multiselect;
|
||||
final bool initialized;
|
||||
final WrapAlignment alignment;
|
||||
final Color? disabledColor;
|
||||
final bool wrapped;
|
||||
|
||||
@override
|
||||
State<FlutterFlowChoiceChips> createState() => _FlutterFlowChoiceChipsState();
|
||||
}
|
||||
|
||||
class _FlutterFlowChoiceChipsState extends State<FlutterFlowChoiceChips> {
|
||||
late List<String> choiceChipValues;
|
||||
List<String> get selectedValues => widget.controller.value ?? [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
choiceChipValues = List.from(selectedValues);
|
||||
if (!widget.initialized && choiceChipValues.isNotEmpty) {
|
||||
SchedulerBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(choiceChipValues);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = widget.options.map<Widget>(
|
||||
(option) {
|
||||
final selected = selectedValues.contains(option.label);
|
||||
final style =
|
||||
selected ? widget.selectedChipStyle : widget.unselectedChipStyle;
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: ChoiceChip(
|
||||
selected: selected,
|
||||
onSelected: widget.onChanged != null
|
||||
? (isSelected) {
|
||||
choiceChipValues = List.from(selectedValues);
|
||||
if (isSelected) {
|
||||
widget.multiselect
|
||||
? choiceChipValues.add(option.label)
|
||||
: choiceChipValues = [option.label];
|
||||
widget.controller.value = List.from(choiceChipValues);
|
||||
setState(() {});
|
||||
} else {
|
||||
if (widget.multiselect) {
|
||||
choiceChipValues.remove(option.label);
|
||||
widget.controller.value = List.from(choiceChipValues);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
widget.onChanged!(choiceChipValues);
|
||||
}
|
||||
: null,
|
||||
label: Text(
|
||||
option.label,
|
||||
style: style.textStyle,
|
||||
),
|
||||
labelPadding: style.labelPadding,
|
||||
avatar: option.iconData != null
|
||||
? FaIcon(
|
||||
option.iconData,
|
||||
size: style.iconSize,
|
||||
color: style.iconColor,
|
||||
)
|
||||
: null,
|
||||
elevation: style.elevation,
|
||||
disabledColor: widget.disabledColor,
|
||||
selectedColor:
|
||||
selected ? widget.selectedChipStyle.backgroundColor : null,
|
||||
backgroundColor:
|
||||
selected ? null : widget.unselectedChipStyle.backgroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: style.borderRadius ?? BorderRadius.circular(16),
|
||||
side: BorderSide(
|
||||
color: style.borderColor ?? Colors.transparent,
|
||||
width: style.borderWidth ?? 0,
|
||||
),
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
|
||||
if (widget.wrapped) {
|
||||
return Wrap(
|
||||
spacing: widget.chipSpacing,
|
||||
runSpacing: widget.rowSpacing,
|
||||
alignment: widget.alignment,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: children,
|
||||
);
|
||||
} else {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
clipBehavior: Clip.none,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children.divide(
|
||||
SizedBox(width: widget.chipSpacing),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
lib/src/widgets/flutter_flow_count_controller.dart
Normal file
72
lib/src/widgets/flutter_flow_count_controller.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FlutterFlowCountController extends StatefulWidget {
|
||||
const FlutterFlowCountController({
|
||||
super.key,
|
||||
required this.decrementIconBuilder,
|
||||
required this.incrementIconBuilder,
|
||||
required this.countBuilder,
|
||||
required this.count,
|
||||
required this.updateCount,
|
||||
this.stepSize = 1,
|
||||
this.minimum,
|
||||
this.maximum,
|
||||
this.contentPadding = const EdgeInsets.symmetric(horizontal: 25.0),
|
||||
});
|
||||
|
||||
final Widget Function(bool enabled) decrementIconBuilder;
|
||||
final Widget Function(bool enabled) incrementIconBuilder;
|
||||
final Widget Function(int count) countBuilder;
|
||||
final int count;
|
||||
final Function(int) updateCount;
|
||||
final int stepSize;
|
||||
final int? minimum;
|
||||
final int? maximum;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
|
||||
@override
|
||||
State<FlutterFlowCountController> createState() =>
|
||||
_FlutterFlowCountControllerState();
|
||||
}
|
||||
|
||||
class _FlutterFlowCountControllerState
|
||||
extends State<FlutterFlowCountController> {
|
||||
int get count => widget.count;
|
||||
int? get minimum => widget.minimum;
|
||||
int? get maximum => widget.maximum;
|
||||
int get stepSize => widget.stepSize;
|
||||
|
||||
bool get canDecrement => minimum == null || count - stepSize >= minimum!;
|
||||
bool get canIncrement => maximum == null || count + stepSize <= maximum!;
|
||||
|
||||
void _decrementCounter() {
|
||||
if (canDecrement) {
|
||||
setState(() => widget.updateCount(count - stepSize));
|
||||
}
|
||||
}
|
||||
|
||||
void _incrementCounter() {
|
||||
if (canIncrement) {
|
||||
setState(() => widget.updateCount(count + stepSize));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: widget.contentPadding,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: _decrementCounter,
|
||||
child: widget.decrementIconBuilder(canDecrement),
|
||||
),
|
||||
widget.countBuilder(count),
|
||||
InkWell(
|
||||
onTap: _incrementCounter,
|
||||
child: widget.incrementIconBuilder(canIncrement),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
305
lib/src/widgets/flutter_flow_credit_card_form.dart
Normal file
305
lib/src/widgets/flutter_flow_credit_card_form.dart
Normal file
@@ -0,0 +1,305 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_credit_card/flutter_credit_card.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:flutter_credit_card/src/masked_text_controller.dart';
|
||||
|
||||
export 'package:flutter_credit_card/flutter_credit_card.dart'
|
||||
show CreditCardModel;
|
||||
|
||||
/// Modified from https://pub.dev/packages/flutter_credit_card (see license below)
|
||||
|
||||
CreditCardModel emptyCreditCard() => CreditCardModel('', '', '', '', false);
|
||||
|
||||
/// A form widget for entering credit card information.
|
||||
class FlutterFlowCreditCardForm extends StatefulWidget {
|
||||
/// Creates a [FlutterFlowCreditCardForm].
|
||||
///
|
||||
/// - [formKey] is a global key that uniquely identifies the form.
|
||||
/// - [creditCardModel] is the model that holds the credit card information.
|
||||
/// - [obscureNumber] determines whether the credit card number should be obscured.
|
||||
/// - [obscureCvv] determines whether the CVV should be obscured.
|
||||
/// - [textStyle] is the style of the text in the form.
|
||||
/// - [spacing] is the spacing between form fields.
|
||||
/// - [inputDecoration] is the decoration for the form fields.
|
||||
const FlutterFlowCreditCardForm({
|
||||
super.key,
|
||||
required this.formKey,
|
||||
required this.creditCardModel,
|
||||
this.obscureNumber = false,
|
||||
this.obscureCvv = false,
|
||||
this.textStyle,
|
||||
this.spacing = 10.0,
|
||||
this.inputDecoration = const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
});
|
||||
|
||||
final GlobalKey<FormState> formKey;
|
||||
final CreditCardModel creditCardModel;
|
||||
final bool obscureNumber;
|
||||
final bool obscureCvv;
|
||||
final TextStyle? textStyle;
|
||||
final double spacing;
|
||||
final InputDecoration inputDecoration;
|
||||
|
||||
@override
|
||||
State<FlutterFlowCreditCardForm> createState() =>
|
||||
_FlutterFlowCreditCardFormState();
|
||||
}
|
||||
|
||||
class _FlutterFlowCreditCardFormState extends State<FlutterFlowCreditCardForm> {
|
||||
final TextEditingController _cardNumberController =
|
||||
MaskedTextController(mask: '0000 0000 0000 0000');
|
||||
final TextEditingController _expiryDateController =
|
||||
MaskedTextController(mask: '00/00');
|
||||
final TextEditingController _cvvCodeController =
|
||||
MaskedTextController(mask: '0000');
|
||||
|
||||
FocusNode cvvFocusNode = FocusNode();
|
||||
FocusNode cardNumberNode = FocusNode();
|
||||
FocusNode expiryDateNode = FocusNode();
|
||||
|
||||
String get cardNumber => widget.creditCardModel.cardNumber;
|
||||
|
||||
void textFieldFocusDidChange() {
|
||||
widget.creditCardModel.isCvvFocused = cvvFocusNode.hasFocus;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.creditCardModel.cardNumber.isNotEmpty) {
|
||||
_cardNumberController.text = widget.creditCardModel.cardNumber;
|
||||
}
|
||||
if (widget.creditCardModel.expiryDate.isNotEmpty) {
|
||||
_expiryDateController.text = widget.creditCardModel.expiryDate;
|
||||
}
|
||||
if (widget.creditCardModel.cvvCode.isNotEmpty) {
|
||||
_cvvCodeController.text = widget.creditCardModel.cvvCode;
|
||||
}
|
||||
cvvFocusNode.addListener(textFieldFocusDidChange);
|
||||
_cardNumberController.addListener(() => setState(
|
||||
() => widget.creditCardModel.cardNumber = _cardNumberController.text));
|
||||
_expiryDateController.addListener(() => setState(
|
||||
() => widget.creditCardModel.expiryDate = _expiryDateController.text));
|
||||
_cvvCodeController.addListener(() => setState(
|
||||
() => widget.creditCardModel.cvvCode = _cvvCodeController.text));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cvvFocusNode.dispose();
|
||||
expiryDateNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Form(
|
||||
key: widget.formKey,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 12.0),
|
||||
child: TextFormField(
|
||||
obscureText: widget.obscureNumber,
|
||||
controller: _cardNumberController,
|
||||
onEditingComplete: () =>
|
||||
FocusScope.of(context).requestFocus(expiryDateNode),
|
||||
style: widget.textStyle,
|
||||
decoration: widget.inputDecoration.copyWith(
|
||||
labelText: 'Card number',
|
||||
hintText: 'XXXX XXXX XXXX XXXX',
|
||||
labelStyle: widget.textStyle,
|
||||
hintStyle: widget.textStyle,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
// Validate less that 13 digits +3 white spaces
|
||||
if (value == null || value.isEmpty || value.length < 16) {
|
||||
return 'Please input a valid number';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: widget.spacing),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 8.0, bottom: 8.0),
|
||||
child: TextFormField(
|
||||
controller: _expiryDateController,
|
||||
focusNode: expiryDateNode,
|
||||
onEditingComplete: () {
|
||||
FocusScope.of(context).requestFocus(cvvFocusNode);
|
||||
},
|
||||
style: widget.textStyle,
|
||||
decoration: widget.inputDecoration.copyWith(
|
||||
labelText: 'Exp. Date',
|
||||
hintText: 'MM/YY',
|
||||
labelStyle: widget.textStyle,
|
||||
hintStyle: widget.textStyle,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please input a valid date';
|
||||
}
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
final List<String> date = value.split(RegExp(r'/'));
|
||||
final int month = int.parse(date.first);
|
||||
final int year = int.parse('20\${date.last}');
|
||||
final DateTime cardDate = DateTime(year, month);
|
||||
|
||||
if (cardDate.isBefore(now) ||
|
||||
month > 12 ||
|
||||
month == 0) {
|
||||
return 'Please input a valid date';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 8.0, bottom: 8.0),
|
||||
child: TextFormField(
|
||||
obscureText: widget.obscureCvv,
|
||||
focusNode: cvvFocusNode,
|
||||
controller: _cvvCodeController,
|
||||
style: widget.textStyle,
|
||||
decoration: widget.inputDecoration.copyWith(
|
||||
labelText: 'CVV',
|
||||
hintText: 'XXX',
|
||||
labelStyle: widget.textStyle,
|
||||
hintStyle: widget.textStyle,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
value.length < 3) {
|
||||
return 'Please input a valid CVV';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
/// Credit Card prefix patterns as of March 2019
|
||||
/// A [List<String>] represents a range.
|
||||
/// i.e. ['51', '55'] represents the range of cards starting with '51' to those starting with '55'
|
||||
Map<CardType, Set<List<String>>> cardNumPatterns =
|
||||
<CardType, Set<List<String>>>{
|
||||
CardType.visa: <List<String>>{
|
||||
<String>['4'],
|
||||
},
|
||||
CardType.americanExpress: <List<String>>{
|
||||
<String>['34'],
|
||||
<String>['37'],
|
||||
},
|
||||
CardType.discover: <List<String>>{
|
||||
<String>['6011'],
|
||||
<String>['622126', '622925'],
|
||||
<String>['644', '649'],
|
||||
<String>['65']
|
||||
},
|
||||
CardType.mastercard: <List<String>>{
|
||||
<String>['51', '55'],
|
||||
<String>['2221', '2229'],
|
||||
<String>['223', '229'],
|
||||
<String>['23', '26'],
|
||||
<String>['270', '271'],
|
||||
<String>['2720'],
|
||||
},
|
||||
};
|
||||
|
||||
/// This function determines the Credit Card type based on the cardPatterns
|
||||
/// and returns it.
|
||||
CardType detectCCType(String cardNumber) {
|
||||
//Default card type is other
|
||||
CardType cardType = CardType.otherBrand;
|
||||
|
||||
if (cardNumber.isEmpty) {
|
||||
return cardType;
|
||||
}
|
||||
|
||||
cardNumPatterns.forEach(
|
||||
(type, patterns) {
|
||||
for (final patternRange in patterns) {
|
||||
// Remove any spaces
|
||||
String ccPatternStr = cardNumber.replaceAll(RegExp(r's+\b|\bs'), '');
|
||||
final int rangeLen = patternRange[0].length;
|
||||
// Trim the Credit Card number string to match the pattern prefix length
|
||||
if (rangeLen < cardNumber.length) {
|
||||
ccPatternStr = ccPatternStr.substring(0, rangeLen);
|
||||
}
|
||||
|
||||
if (patternRange.length > 1) {
|
||||
// Convert the prefix range into numbers then make sure the
|
||||
// Credit Card num is in the pattern range.
|
||||
// Because Strings don't have '>=' type operators
|
||||
final int ccPrefixAsInt = int.parse(ccPatternStr);
|
||||
final int startPatternPrefixAsInt = int.parse(patternRange[0]);
|
||||
final int endPatternPrefixAsInt = int.parse(patternRange[1]);
|
||||
if (ccPrefixAsInt >= startPatternPrefixAsInt &&
|
||||
ccPrefixAsInt <= endPatternPrefixAsInt) {
|
||||
// Found a match
|
||||
cardType = type;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Just compare the single pattern prefix with the Credit Card prefix
|
||||
if (ccPatternStr == patternRange[0]) {
|
||||
// Found a match
|
||||
cardType = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return cardType;
|
||||
}
|
||||
}
|
||||
|
||||
/// BSD 2-Clause License
|
||||
|
||||
/// Copyright (c) 2019, Simform Solutions
|
||||
/// All rights reserved.
|
||||
|
||||
/// Redistribution and use in source and binary forms, with or without
|
||||
/// modification, are permitted provided that the following conditions are met:
|
||||
|
||||
/// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
/// list of conditions and the following disclaimer.
|
||||
|
||||
/// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
/// this list of conditions and the following disclaimer in the documentation
|
||||
/// and/or other materials provided with the distribution.
|
||||
|
||||
/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
/// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
/// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
/// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
/// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
/// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
/// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
/// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
/// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
/// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
342
lib/src/widgets/flutter_flow_data_table.dart
Normal file
342
lib/src/widgets/flutter_flow_data_table.dart
Normal file
@@ -0,0 +1,342 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:data_table_2/data_table_2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
export 'package:data_table_2/data_table_2.dart' show DataColumn2;
|
||||
|
||||
const _kDataTableHorizontalMargin = 48.0;
|
||||
const kDefaultColumnSpacing = 56.0;
|
||||
const _kMinRowsPerPage = 5;
|
||||
|
||||
typedef ColumnsBuilder<T> = List<DataColumn> Function(void Function(int, bool));
|
||||
typedef DataRowBuilder<T> = DataRow? Function(
|
||||
T, int, bool, void Function(bool?)?);
|
||||
|
||||
class FlutterFlowDataTableController<T> extends DataTableSource {
|
||||
FlutterFlowDataTableController({
|
||||
List<T>? initialData,
|
||||
int? numRows,
|
||||
PaginatorController? paginatorController,
|
||||
bool selectable = false,
|
||||
}) {
|
||||
data = initialData?.toList() ?? [];
|
||||
numRows = numRows;
|
||||
this.paginatorController = paginatorController ?? PaginatorController();
|
||||
_selectable = selectable;
|
||||
}
|
||||
|
||||
DataRowBuilder<T>? _dataRowBuilder;
|
||||
late PaginatorController paginatorController;
|
||||
List<T> data = [];
|
||||
int? _numRows;
|
||||
List<T> get selectedData =>
|
||||
selectedRows.where((i) => i < data.length).map(data.elementAt).toList();
|
||||
|
||||
bool _selectable = false;
|
||||
final Set<int> selectedRows = {};
|
||||
|
||||
int rowsPerPage = defaultRowsPerPage;
|
||||
int? sortColumnIndex;
|
||||
bool sortAscending = true;
|
||||
|
||||
void init({
|
||||
DataRowBuilder<T>? dataRowBuilder,
|
||||
bool? selectable,
|
||||
List<T>? initialData,
|
||||
int? initialNumRows,
|
||||
}) {
|
||||
_dataRowBuilder = dataRowBuilder ?? _dataRowBuilder;
|
||||
_selectable = selectable ?? _selectable;
|
||||
data = initialData?.toList() ?? data;
|
||||
_numRows = initialNumRows;
|
||||
}
|
||||
|
||||
void updateData({
|
||||
List<T>? data,
|
||||
int? numRows,
|
||||
bool notify = true,
|
||||
}) {
|
||||
this.data = data?.toList() ?? this.data;
|
||||
_numRows = numRows ?? _numRows;
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void updateSort({
|
||||
required int columnIndex,
|
||||
required bool ascending,
|
||||
Function(int, bool)? onSortChanged,
|
||||
}) {
|
||||
sortColumnIndex = columnIndex;
|
||||
sortAscending = ascending;
|
||||
if (onSortChanged != null) {
|
||||
onSortChanged(columnIndex, ascending);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
DataRow? getRow(int index) {
|
||||
final row = data.elementAtOrNull(index);
|
||||
return _dataRowBuilder != null && row != null
|
||||
? _dataRowBuilder!(
|
||||
row,
|
||||
index,
|
||||
selectedRows.contains(index),
|
||||
_selectable
|
||||
? (selected) {
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
selected
|
||||
? selectedRows.add(index)
|
||||
: selectedRows.remove(index);
|
||||
notifyListeners();
|
||||
}
|
||||
: null,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRowCountApproximate => false;
|
||||
|
||||
@override
|
||||
int get rowCount => _numRows ?? data.length;
|
||||
|
||||
@override
|
||||
int get selectedRowCount => selectedRows.length;
|
||||
}
|
||||
|
||||
/// A widget that displays tabular data in a grid format.
|
||||
class FlutterFlowDataTable<T> extends StatefulWidget {
|
||||
const FlutterFlowDataTable({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.data,
|
||||
this.numRows,
|
||||
required this.columnsBuilder,
|
||||
required this.dataRowBuilder,
|
||||
this.emptyBuilder,
|
||||
this.onPageChanged,
|
||||
this.onSortChanged,
|
||||
this.onRowsPerPageChanged,
|
||||
this.paginated = true,
|
||||
this.selectable = false,
|
||||
this.hidePaginator = false,
|
||||
this.showFirstLastButtons = false,
|
||||
this.width,
|
||||
this.height,
|
||||
this.minWidth,
|
||||
this.headingRowHeight = 56,
|
||||
this.dataRowHeight = kMinInteractiveDimension,
|
||||
this.columnSpacing = kDefaultColumnSpacing,
|
||||
this.headingRowColor,
|
||||
this.sortIconColor,
|
||||
this.borderRadius,
|
||||
this.addHorizontalDivider = true,
|
||||
this.addTopAndBottomDivider = false,
|
||||
this.hideDefaultHorizontalDivider = false,
|
||||
this.addVerticalDivider = false,
|
||||
this.horizontalDividerColor,
|
||||
this.horizontalDividerThickness,
|
||||
this.verticalDividerColor,
|
||||
this.verticalDividerThickness,
|
||||
this.checkboxUnselectedFillColor,
|
||||
this.checkboxSelectedFillColor,
|
||||
this.checkboxUnselectedBorderColor,
|
||||
this.checkboxSelectedBorderColor,
|
||||
this.checkboxCheckColor,
|
||||
});
|
||||
|
||||
final FlutterFlowDataTableController<T> controller;
|
||||
final List<T> data;
|
||||
final int? numRows;
|
||||
final ColumnsBuilder columnsBuilder;
|
||||
final DataRowBuilder<T> dataRowBuilder;
|
||||
final Widget? Function()? emptyBuilder;
|
||||
// Callback functions
|
||||
final Function(int)? onPageChanged;
|
||||
final Function(int, bool)? onSortChanged;
|
||||
final Function(int)? onRowsPerPageChanged;
|
||||
// Functionality options
|
||||
final bool paginated;
|
||||
final bool selectable;
|
||||
final bool showFirstLastButtons;
|
||||
final bool hidePaginator;
|
||||
// Size and shape options
|
||||
final double? width;
|
||||
final double? height;
|
||||
final double? minWidth;
|
||||
final double headingRowHeight;
|
||||
final double dataRowHeight;
|
||||
final double columnSpacing;
|
||||
// Table style options
|
||||
final Color? headingRowColor;
|
||||
final Color? sortIconColor;
|
||||
final BorderRadius? borderRadius;
|
||||
final bool addHorizontalDivider;
|
||||
final bool addTopAndBottomDivider;
|
||||
final bool hideDefaultHorizontalDivider;
|
||||
final Color? horizontalDividerColor;
|
||||
final double? horizontalDividerThickness;
|
||||
final bool addVerticalDivider;
|
||||
final Color? verticalDividerColor;
|
||||
final double? verticalDividerThickness;
|
||||
// Checkbox style options
|
||||
final Color? checkboxUnselectedFillColor;
|
||||
final Color? checkboxSelectedFillColor;
|
||||
final Color? checkboxUnselectedBorderColor;
|
||||
final Color? checkboxSelectedBorderColor;
|
||||
final Color? checkboxCheckColor;
|
||||
|
||||
@override
|
||||
State<FlutterFlowDataTable<T>> createState() =>
|
||||
_FlutterFlowDataTableState<T>();
|
||||
}
|
||||
|
||||
class _FlutterFlowDataTableState<T> extends State<FlutterFlowDataTable<T>> {
|
||||
FlutterFlowDataTableController<T> get controller => widget.controller;
|
||||
int get rowCount => controller.rowCount;
|
||||
|
||||
int get initialRowsPerPage =>
|
||||
rowCount > _kMinRowsPerPage ? defaultRowsPerPage : _kMinRowsPerPage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
dataTableShowLogs = false; // Disables noisy DataTable2 debug statements.
|
||||
controller.init(
|
||||
dataRowBuilder: widget.dataRowBuilder,
|
||||
selectable: widget.selectable,
|
||||
initialData: widget.data,
|
||||
initialNumRows: widget.numRows,
|
||||
);
|
||||
// ignore: cascade_invocations
|
||||
controller.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FlutterFlowDataTable<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
controller.updateData(
|
||||
data: widget.data,
|
||||
numRows: widget.numRows,
|
||||
notify: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final columns = widget.columnsBuilder(
|
||||
(index, ascending) {
|
||||
controller.updateSort(
|
||||
columnIndex: index,
|
||||
ascending: ascending,
|
||||
onSortChanged: widget.onSortChanged,
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
|
||||
final checkboxThemeData = CheckboxThemeData(
|
||||
checkColor: WidgetStateProperty.all(
|
||||
widget.checkboxCheckColor ?? Colors.black54,
|
||||
),
|
||||
fillColor: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.selected)
|
||||
? widget.checkboxSelectedFillColor ?? Colors.white.withOpacity(0.01)
|
||||
: widget.checkboxUnselectedFillColor ??
|
||||
Colors.white.withOpacity(0.01),
|
||||
),
|
||||
side: WidgetStateBorderSide.resolveWith(
|
||||
(states) => BorderSide(
|
||||
width: 2.0,
|
||||
color: states.contains(WidgetState.selected)
|
||||
? widget.checkboxSelectedBorderColor ?? Colors.black54
|
||||
: widget.checkboxUnselectedBorderColor ?? Colors.black54,
|
||||
),
|
||||
),
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
);
|
||||
|
||||
final horizontalBorder = widget.addHorizontalDivider
|
||||
? BorderSide(
|
||||
color: widget.horizontalDividerColor ?? Colors.transparent,
|
||||
width: widget.horizontalDividerThickness ?? 1.0,
|
||||
)
|
||||
: BorderSide.none;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: widget.borderRadius ?? BorderRadius.zero,
|
||||
child: SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
iconTheme: widget.sortIconColor != null
|
||||
? IconThemeData(color: widget.sortIconColor)
|
||||
: null,
|
||||
),
|
||||
child: PaginatedDataTable2(
|
||||
source: controller,
|
||||
controller:
|
||||
widget.paginated ? controller.paginatorController : null,
|
||||
rowsPerPage: widget.paginated ? initialRowsPerPage : rowCount,
|
||||
availableRowsPerPage: const [5, 10, 25, 50, 100],
|
||||
onPageChanged: widget.onPageChanged != null
|
||||
? (index) => widget.onPageChanged!(index)
|
||||
: null,
|
||||
columnSpacing: widget.columnSpacing,
|
||||
onRowsPerPageChanged: widget.paginated
|
||||
? (value) {
|
||||
controller.rowsPerPage = value ?? initialRowsPerPage;
|
||||
if (widget.onRowsPerPageChanged != null) {
|
||||
widget.onRowsPerPageChanged!(controller.rowsPerPage);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
columns: columns,
|
||||
empty: widget.emptyBuilder != null ? widget.emptyBuilder!() : null,
|
||||
sortColumnIndex: controller.sortColumnIndex,
|
||||
sortAscending: controller.sortAscending,
|
||||
showCheckboxColumn: widget.selectable,
|
||||
datarowCheckboxTheme: checkboxThemeData,
|
||||
headingCheckboxTheme: checkboxThemeData,
|
||||
hidePaginator: !widget.paginated || widget.hidePaginator,
|
||||
wrapInCard: false,
|
||||
renderEmptyRowsInTheEnd: false,
|
||||
border: TableBorder(
|
||||
horizontalInside: horizontalBorder,
|
||||
verticalInside: widget.addVerticalDivider
|
||||
? BorderSide(
|
||||
color: widget.verticalDividerColor ?? Colors.transparent,
|
||||
width: widget.verticalDividerThickness ?? 1.0,
|
||||
)
|
||||
: BorderSide.none,
|
||||
bottom: widget.addTopAndBottomDivider
|
||||
? horizontalBorder
|
||||
: BorderSide.none,
|
||||
),
|
||||
dividerThickness: widget.hideDefaultHorizontalDivider ? 0.0 : null,
|
||||
headingRowColor: WidgetStateProperty.all(widget.headingRowColor),
|
||||
headingRowHeight: widget.headingRowHeight,
|
||||
dataRowHeight: widget.dataRowHeight,
|
||||
showFirstLastButtons: widget.showFirstLastButtons,
|
||||
minWidth: math.max(widget.minWidth ?? 0, _getColumnsWidth(columns)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Return the total fixed width of all columns that have a specified width,
|
||||
// plus one to make the data table scrollable if there is insufficient space.
|
||||
double _getColumnsWidth(List<DataColumn> columns) =>
|
||||
columns.where((c) => c is DataColumn2 && c.fixedWidth != null).fold(
|
||||
((widget.selectable ? 2 : 1) * _kDataTableHorizontalMargin) + 1,
|
||||
(sum, col) => sum + (col as DataColumn2).fixedWidth!,
|
||||
);
|
||||
}
|
||||
433
lib/src/widgets/flutter_flow_drop_down.dart
Normal file
433
lib/src/widgets/flutter_flow_drop_down.dart
Normal file
@@ -0,0 +1,433 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../utils/form_field_controller.dart';
|
||||
|
||||
/// A dropdown widget that allows the user to select an option from a list of options.
|
||||
class FlutterFlowDropDown<T> extends StatefulWidget {
|
||||
const FlutterFlowDropDown({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.multiSelectController,
|
||||
this.hintText,
|
||||
this.searchHintText,
|
||||
required this.options,
|
||||
this.optionLabels,
|
||||
this.onChanged,
|
||||
this.onMultiSelectChanged,
|
||||
this.icon,
|
||||
this.width,
|
||||
this.height,
|
||||
this.maxHeight,
|
||||
this.fillColor,
|
||||
this.searchHintTextStyle,
|
||||
this.searchTextStyle,
|
||||
this.searchCursorColor,
|
||||
required this.textStyle,
|
||||
required this.elevation,
|
||||
required this.borderWidth,
|
||||
required this.borderRadius,
|
||||
required this.borderColor,
|
||||
required this.margin,
|
||||
this.hidesUnderline = false,
|
||||
this.disabled = false,
|
||||
this.isOverButton = false,
|
||||
this.menuOffset,
|
||||
this.isSearchable = false,
|
||||
this.isMultiSelect = false,
|
||||
this.labelText,
|
||||
this.labelTextStyle,
|
||||
}) : assert(
|
||||
isMultiSelect
|
||||
? (controller == null &&
|
||||
onChanged == null &&
|
||||
multiSelectController != null &&
|
||||
onMultiSelectChanged != null)
|
||||
: (controller != null &&
|
||||
onChanged != null &&
|
||||
multiSelectController == null &&
|
||||
onMultiSelectChanged == null),
|
||||
);
|
||||
|
||||
/// The controller for the dropdown field.
|
||||
final FormFieldController<T?>? controller;
|
||||
|
||||
/// The controller for the multi-select dropdown field.
|
||||
final FormFieldController<List<T>?>? multiSelectController;
|
||||
|
||||
/// The text to display as a hint when no option is selected.
|
||||
final String? hintText;
|
||||
|
||||
/// The text to display as a hint in the search field.
|
||||
final String? searchHintText;
|
||||
|
||||
/// The list of options to display in the dropdown.
|
||||
final List<T> options;
|
||||
|
||||
/// The list of labels corresponding to the options.
|
||||
final List<String>? optionLabels;
|
||||
|
||||
/// A callback function that is called when the selected option changes.
|
||||
final Function(T?)? onChanged;
|
||||
|
||||
/// A callback function that is called when the selected options change in multi-select mode.
|
||||
final Function(List<T>?)? onMultiSelectChanged;
|
||||
|
||||
/// The icon to display in the dropdown field.
|
||||
final Widget? icon;
|
||||
|
||||
/// The width of the dropdown field.
|
||||
final double? width;
|
||||
|
||||
/// The height of the dropdown field.
|
||||
final double? height;
|
||||
|
||||
/// The maximum height of the dropdown menu.
|
||||
final double? maxHeight;
|
||||
|
||||
/// The background color of the dropdown field.
|
||||
final Color? fillColor;
|
||||
|
||||
/// The text style for the search hint text.
|
||||
final TextStyle? searchHintTextStyle;
|
||||
|
||||
/// The text style for the search text.
|
||||
final TextStyle? searchTextStyle;
|
||||
|
||||
/// The color of the search cursor.
|
||||
final Color? searchCursorColor;
|
||||
|
||||
/// The text style for the dropdown field.
|
||||
final TextStyle textStyle;
|
||||
|
||||
/// The elevation of the dropdown menu.
|
||||
final double elevation;
|
||||
|
||||
/// The width of the dropdown field's border.
|
||||
final double borderWidth;
|
||||
|
||||
/// The border radius of the dropdown field.
|
||||
final double borderRadius;
|
||||
|
||||
/// The color of the dropdown field's border.
|
||||
final Color borderColor;
|
||||
|
||||
/// The margin around the dropdown field.
|
||||
final EdgeInsetsGeometry margin;
|
||||
|
||||
/// Whether to hide the underline of the dropdown field.
|
||||
final bool hidesUnderline;
|
||||
|
||||
/// Whether the dropdown is disabled.
|
||||
final bool disabled;
|
||||
|
||||
/// Whether the dropdown menu is displayed over the button.
|
||||
final bool isOverButton;
|
||||
|
||||
/// The offset of the dropdown menu.
|
||||
final Offset? menuOffset;
|
||||
|
||||
/// Whether the dropdown is searchable.
|
||||
final bool isSearchable;
|
||||
|
||||
/// Whether the dropdown is in multi-select mode.
|
||||
final bool isMultiSelect;
|
||||
|
||||
/// The label text for the dropdown field.
|
||||
final String? labelText;
|
||||
|
||||
/// The text style for the label text.
|
||||
final TextStyle? labelTextStyle;
|
||||
|
||||
@override
|
||||
State<FlutterFlowDropDown<T>> createState() => _FlutterFlowDropDownState<T>();
|
||||
}
|
||||
|
||||
class _FlutterFlowDropDownState<T> extends State<FlutterFlowDropDown<T>> {
|
||||
bool get isMultiSelect => widget.isMultiSelect;
|
||||
FormFieldController<T?> get controller => widget.controller!;
|
||||
FormFieldController<List<T>?> get multiSelectController =>
|
||||
widget.multiSelectController!;
|
||||
|
||||
T? get currentValue {
|
||||
final value = isMultiSelect
|
||||
? multiSelectController.value?.firstOrNull
|
||||
: controller.value;
|
||||
return widget.options.contains(value) ? value : null;
|
||||
}
|
||||
|
||||
Set<T> get currentValues {
|
||||
if (!isMultiSelect || multiSelectController.value == null) {
|
||||
return {};
|
||||
}
|
||||
return widget.options
|
||||
.toSet()
|
||||
.intersection(multiSelectController.value!.toSet());
|
||||
}
|
||||
|
||||
Map<T, String> get optionLabels => Map.fromEntries(
|
||||
widget.options.asMap().entries.map(
|
||||
(option) => MapEntry(
|
||||
option.value,
|
||||
widget.optionLabels == null ||
|
||||
widget.optionLabels!.length < option.key + 1
|
||||
? option.value.toString()
|
||||
: widget.optionLabels![option.key],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
EdgeInsetsGeometry get horizontalMargin => widget.margin.clamp(
|
||||
EdgeInsetsDirectional.zero,
|
||||
const EdgeInsetsDirectional.symmetric(horizontal: double.infinity),
|
||||
);
|
||||
|
||||
late void Function() _listener;
|
||||
final TextEditingController _textEditingController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (isMultiSelect) {
|
||||
_listener =
|
||||
() => widget.onMultiSelectChanged!(multiSelectController.value);
|
||||
multiSelectController.addListener(_listener);
|
||||
} else {
|
||||
_listener = () => widget.onChanged!(controller.value);
|
||||
controller.addListener(_listener);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (isMultiSelect) {
|
||||
multiSelectController.removeListener(_listener);
|
||||
} else {
|
||||
controller.removeListener(_listener);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dropdownWidget = _buildDropdownWidget();
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
border: Border.all(
|
||||
color: widget.borderColor,
|
||||
width: widget.borderWidth,
|
||||
),
|
||||
color: widget.fillColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: _useDropdown2() ? EdgeInsets.zero : widget.margin,
|
||||
child: widget.hidesUnderline
|
||||
? DropdownButtonHideUnderline(child: dropdownWidget)
|
||||
: dropdownWidget,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _useDropdown2() =>
|
||||
widget.isMultiSelect ||
|
||||
widget.isSearchable ||
|
||||
!widget.isOverButton ||
|
||||
widget.maxHeight != null;
|
||||
|
||||
Widget _buildDropdownWidget() =>
|
||||
_useDropdown2() ? _buildDropdown() : _buildLegacyDropdown();
|
||||
|
||||
Widget _buildLegacyDropdown() {
|
||||
return DropdownButtonFormField<T>(
|
||||
value: currentValue,
|
||||
hint: _createHintText(),
|
||||
items: _createMenuItems(),
|
||||
elevation: widget.elevation.toInt(),
|
||||
onChanged: widget.disabled ? null : (value) => controller.value = value,
|
||||
icon: widget.icon,
|
||||
isExpanded: true,
|
||||
dropdownColor: widget.fillColor,
|
||||
focusColor: Colors.transparent,
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.labelText == null || widget.labelText!.isEmpty
|
||||
? null
|
||||
: widget.labelText,
|
||||
labelStyle: widget.labelTextStyle,
|
||||
border: widget.hidesUnderline
|
||||
? InputBorder.none
|
||||
: const UnderlineInputBorder(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Text? _createHintText() => widget.hintText != null
|
||||
? Text(widget.hintText!, style: widget.textStyle)
|
||||
: null;
|
||||
|
||||
List<DropdownMenuItem<T>> _createMenuItems() => widget.options
|
||||
.map(
|
||||
(option) => DropdownMenuItem<T>(
|
||||
value: option,
|
||||
child: Padding(
|
||||
padding: _useDropdown2() ? horizontalMargin : EdgeInsets.zero,
|
||||
child: Text(optionLabels[option] ?? '', style: widget.textStyle),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
List<DropdownMenuItem<T>> _createMultiselectMenuItems() => widget.options
|
||||
.map(
|
||||
(item) => DropdownMenuItem<T>(
|
||||
value: item,
|
||||
// Disable default onTap to avoid closing menu when selecting an item
|
||||
enabled: false,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, menuSetState) {
|
||||
final isSelected =
|
||||
multiSelectController.value?.contains(item) ?? false;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
multiSelectController.value ??= [];
|
||||
isSelected
|
||||
? multiSelectController.value!.remove(item)
|
||||
: multiSelectController.value!.add(item);
|
||||
multiSelectController.update();
|
||||
// This rebuilds the StatefulWidget to update the button's text.
|
||||
setState(() {});
|
||||
// This rebuilds the dropdownMenu Widget to update the check mark.
|
||||
menuSetState(() {});
|
||||
},
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
padding: horizontalMargin,
|
||||
child: Row(
|
||||
children: [
|
||||
if (isSelected)
|
||||
const Icon(Icons.check_box_outlined)
|
||||
else
|
||||
const Icon(Icons.check_box_outline_blank),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
optionLabels[item]!,
|
||||
style: widget.textStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
Widget _buildDropdown() {
|
||||
final overlayColor = WidgetStateProperty.resolveWith<Color?>((states) =>
|
||||
states.contains(WidgetState.focused) ? Colors.transparent : null);
|
||||
final iconStyleData = widget.icon != null
|
||||
? IconStyleData(icon: widget.icon!)
|
||||
: const IconStyleData();
|
||||
return DropdownButton2<T>(
|
||||
value: currentValue,
|
||||
hint: _createHintText(),
|
||||
items: isMultiSelect ? _createMultiselectMenuItems() : _createMenuItems(),
|
||||
iconStyleData: iconStyleData,
|
||||
buttonStyleData: ButtonStyleData(
|
||||
elevation: widget.elevation.toInt(),
|
||||
overlayColor: overlayColor,
|
||||
padding: widget.margin,
|
||||
),
|
||||
menuItemStyleData: MenuItemStyleData(
|
||||
overlayColor: overlayColor,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
elevation: widget.elevation.toInt(),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: widget.fillColor,
|
||||
),
|
||||
isOverButton: widget.isOverButton,
|
||||
offset: widget.menuOffset ?? Offset.zero,
|
||||
maxHeight: widget.maxHeight,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onChanged: widget.disabled
|
||||
? null
|
||||
: (isMultiSelect ? (_) {} : (val) => widget.controller!.value = val),
|
||||
isExpanded: true,
|
||||
selectedItemBuilder: (context) => widget.options
|
||||
.map(
|
||||
(item) => Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
isMultiSelect
|
||||
? currentValues
|
||||
.where((v) => optionLabels.containsKey(v))
|
||||
.map((v) => optionLabels[v])
|
||||
.join(', ')
|
||||
: optionLabels[item]!,
|
||||
style: widget.textStyle,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
dropdownSearchData: widget.isSearchable
|
||||
? DropdownSearchData<T>(
|
||||
searchController: _textEditingController,
|
||||
searchInnerWidgetHeight: 50,
|
||||
searchInnerWidget: Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8,
|
||||
bottom: 4,
|
||||
right: 8,
|
||||
left: 8,
|
||||
),
|
||||
child: TextFormField(
|
||||
expands: true,
|
||||
maxLines: null,
|
||||
controller: _textEditingController,
|
||||
cursorColor: widget.searchCursorColor,
|
||||
style: widget.searchTextStyle,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
hintText: widget.searchHintText,
|
||||
hintStyle: widget.searchHintTextStyle,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
searchMatchFn: (item, searchValue) {
|
||||
return (optionLabels[item.value] ?? '')
|
||||
.toLowerCase()
|
||||
.contains(searchValue.toLowerCase());
|
||||
},
|
||||
)
|
||||
: null,
|
||||
// This is to clear the search value when you close the menu
|
||||
onMenuStateChange: widget.isSearchable
|
||||
? (isOpen) {
|
||||
if (!isOpen) {
|
||||
_textEditingController.clear();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
73
lib/src/widgets/flutter_flow_expanded_image_view.dart
Normal file
73
lib/src/widgets/flutter_flow_expanded_image_view.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
/// A widget that displays an expanded image view.
|
||||
class FlutterFlowExpandedImageView extends StatelessWidget {
|
||||
/// Creates a [FlutterFlowExpandedImageView].
|
||||
///
|
||||
/// - [image] parameter is required and represents the image to be displayed.
|
||||
/// - [allowRotation] parameter determines whether rotation is allowed for the image.
|
||||
/// - [useHeroAnimation] parameter determines whether to use a hero animation when transitioning to the expanded image view.
|
||||
/// - [tag] parameter is an optional tag used for the hero animation.
|
||||
const FlutterFlowExpandedImageView({
|
||||
super.key,
|
||||
required this.image,
|
||||
this.allowRotation = false,
|
||||
this.useHeroAnimation = true,
|
||||
this.tag,
|
||||
});
|
||||
|
||||
final Widget image;
|
||||
final bool allowRotation;
|
||||
final bool useHeroAnimation;
|
||||
final Object? tag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.sizeOf(context);
|
||||
return Material(
|
||||
color: Colors.black,
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: screenSize.height,
|
||||
width: screenSize.width,
|
||||
child: PhotoView.customChild(
|
||||
minScale: 1.0,
|
||||
maxScale: 3.0,
|
||||
enableRotation: allowRotation,
|
||||
heroAttributes: useHeroAnimation
|
||||
? PhotoViewHeroAttributes(tag: tag!)
|
||||
: null,
|
||||
onScaleEnd: (context, details, value) {
|
||||
if (value.scale! < 0.3) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: image,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: IconButton(
|
||||
color: Colors.black,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 32,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
301
lib/src/widgets/flutter_flow_google_map.dart
Normal file
301
lib/src/widgets/flutter_flow_google_map.dart
Normal file
@@ -0,0 +1,301 @@
|
||||
// import 'dart:async';
|
||||
// import 'dart:math';
|
||||
// import 'dart:ui';
|
||||
|
||||
// import 'package:cached_network_image/cached_network_image.dart';
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/scheduler.dart';
|
||||
// import 'package:flutterflow_ui/src/utils/lat_lng.dart' as latlng;
|
||||
// import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
// export 'dart:async' show Completer;
|
||||
|
||||
// export 'package:flutterflow_ui/src/utils/lat_lng.dart' show LatLng;
|
||||
// export 'package:google_maps_flutter/google_maps_flutter.dart' hide LatLng;
|
||||
|
||||
// enum GoogleMapStyle {
|
||||
// standard,
|
||||
// silver,
|
||||
// retro,
|
||||
// dark,
|
||||
// night,
|
||||
// aubergine,
|
||||
// }
|
||||
|
||||
// enum GoogleMarkerColor {
|
||||
// red,
|
||||
// orange,
|
||||
// yellow,
|
||||
// green,
|
||||
// cyan,
|
||||
// azure,
|
||||
// blue,
|
||||
// violet,
|
||||
// magenta,
|
||||
// rose,
|
||||
// }
|
||||
|
||||
// @immutable
|
||||
// class MarkerImage {
|
||||
// const MarkerImage({
|
||||
// required this.imagePath,
|
||||
// required this.isAssetImage,
|
||||
// this.size = 20.0,
|
||||
// });
|
||||
// final String imagePath;
|
||||
// final bool isAssetImage;
|
||||
// final double size;
|
||||
|
||||
// @override
|
||||
// bool operator ==(Object other) =>
|
||||
// identical(this, other) ||
|
||||
// (other is MarkerImage &&
|
||||
// imagePath == other.imagePath &&
|
||||
// isAssetImage == other.isAssetImage &&
|
||||
// size == other.size);
|
||||
|
||||
// @override
|
||||
// int get hashCode => Object.hash(imagePath, isAssetImage, size);
|
||||
// }
|
||||
|
||||
// class FlutterFlowMarker {
|
||||
// const FlutterFlowMarker(this.markerId, this.location, [this.onTap]);
|
||||
// final String markerId;
|
||||
// final latlng.LatLng location;
|
||||
// final Future Function()? onTap;
|
||||
// }
|
||||
|
||||
// /// A widget that displays a Google Map.
|
||||
// class FlutterFlowGoogleMap extends StatefulWidget {
|
||||
// /// Creates a [FlutterFlowGoogleMap] widget.
|
||||
// const FlutterFlowGoogleMap({
|
||||
// required this.controller,
|
||||
// this.onCameraIdle,
|
||||
// this.initialLocation,
|
||||
// this.markers = const [],
|
||||
// this.markerColor = GoogleMarkerColor.red,
|
||||
// this.markerImage,
|
||||
// this.mapType = MapType.normal,
|
||||
// this.style = GoogleMapStyle.standard,
|
||||
// this.initialZoom = 12,
|
||||
// this.allowInteraction = true,
|
||||
// this.allowZoom = true,
|
||||
// this.showZoomControls = true,
|
||||
// this.showLocation = true,
|
||||
// this.showCompass = false,
|
||||
// this.showMapToolbar = false,
|
||||
// this.showTraffic = false,
|
||||
// this.centerMapOnMarkerTap = false,
|
||||
// super.key,
|
||||
// });
|
||||
|
||||
// /// A [Completer] that completes with a [GoogleMapController] instance.
|
||||
// final Completer<GoogleMapController> controller;
|
||||
|
||||
// /// An optional callback function that will be called when the camera movement
|
||||
// /// has ended and the map is idle.
|
||||
// final Function(latlng.LatLng)? onCameraIdle;
|
||||
|
||||
// /// The initial location to center the map on.
|
||||
// final latlng.LatLng? initialLocation;
|
||||
|
||||
// /// An iterable of [FlutterFlowMarker] objects that represent markers to be
|
||||
// /// displayed on the map.
|
||||
// final Iterable<FlutterFlowMarker> markers;
|
||||
|
||||
// /// The color of the markers.
|
||||
// final GoogleMarkerColor markerColor;
|
||||
|
||||
// /// A custom image to be used as the marker icon.
|
||||
// final MarkerImage? markerImage;
|
||||
|
||||
// /// The type of map to be displayed.
|
||||
// final MapType mapType;
|
||||
|
||||
// /// The style of the map.
|
||||
// final GoogleMapStyle style;
|
||||
|
||||
// /// The initial zoom level of the map.
|
||||
// final double initialZoom;
|
||||
|
||||
// /// Determines whether the user can interact with the map.
|
||||
// final bool allowInteraction;
|
||||
|
||||
// /// Determines whether the user can zoom in and out of the map.
|
||||
// final bool allowZoom;
|
||||
|
||||
// /// Determines whether to show zoom controls on the map.
|
||||
// final bool showZoomControls;
|
||||
|
||||
// /// Determines whether to show the user's current location on the map.
|
||||
// final bool showLocation;
|
||||
|
||||
// /// Determines whether to show a compass on the map.
|
||||
// final bool showCompass;
|
||||
|
||||
// /// Determines whether to show a toolbar with map-related actions.
|
||||
// final bool showMapToolbar;
|
||||
|
||||
// /// Determines whether to show traffic data on the map.
|
||||
// final bool showTraffic;
|
||||
|
||||
// /// Determines whether to center the map on the tapped marker.
|
||||
// final bool centerMapOnMarkerTap;
|
||||
|
||||
// @override
|
||||
// State<StatefulWidget> createState() => _FlutterFlowGoogleMapState();
|
||||
// }
|
||||
|
||||
// class _FlutterFlowGoogleMapState extends State<FlutterFlowGoogleMap> {
|
||||
// double get initialZoom => max(double.minPositive, widget.initialZoom);
|
||||
// LatLng get initialPosition =>
|
||||
// widget.initialLocation?.toGoogleMaps() ?? const LatLng(0.0, 0.0);
|
||||
|
||||
// late Completer<GoogleMapController> _controller;
|
||||
// BitmapDescriptor? _markerDescriptor;
|
||||
// late LatLng currentMapCenter;
|
||||
|
||||
// void initializeMarkerBitmap() {
|
||||
// final markerImage = widget.markerImage;
|
||||
|
||||
// if (markerImage == null) {
|
||||
// _markerDescriptor = BitmapDescriptor.defaultMarkerWithHue(
|
||||
// googleMarkerColorMap[widget.markerColor]!,
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
// final markerImageSize = Size.square(markerImage.size);
|
||||
// var imageProvider = markerImage.isAssetImage
|
||||
// ? Image.asset(markerImage.imagePath).image
|
||||
// : CachedNetworkImageProvider(markerImage.imagePath);
|
||||
// if (!kIsWeb) {
|
||||
// // workaround for https://github.com/flutter/flutter/issues/34657 to
|
||||
// // enable marker resizing on Android and iOS.
|
||||
// final targetHeight =
|
||||
// (markerImage.size * MediaQuery.of(context).devicePixelRatio)
|
||||
// .toInt();
|
||||
// imageProvider = ResizeImage(
|
||||
// imageProvider,
|
||||
// height: targetHeight,
|
||||
// policy: ResizeImagePolicy.fit,
|
||||
// allowUpscaling: true,
|
||||
// );
|
||||
// }
|
||||
// final imageConfiguration =
|
||||
// createLocalImageConfiguration(context, size: markerImageSize);
|
||||
// imageProvider
|
||||
// .resolve(imageConfiguration)
|
||||
// .addListener(ImageStreamListener((img, _) async {
|
||||
// final bytes = await img.image.toByteData(format: ImageByteFormat.png);
|
||||
// if (bytes != null && mounted) {
|
||||
// _markerDescriptor = BitmapDescriptor.fromBytes(
|
||||
// bytes.buffer.asUint8List(),
|
||||
// size: markerImageSize,
|
||||
// );
|
||||
// setState(() {});
|
||||
// }
|
||||
// }));
|
||||
// });
|
||||
// }
|
||||
|
||||
// void onCameraIdle() => widget.onCameraIdle?.call(currentMapCenter.toLatLng());
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// currentMapCenter = initialPosition;
|
||||
// _controller = widget.controller;
|
||||
// initializeMarkerBitmap();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void didUpdateWidget(FlutterFlowGoogleMap oldWidget) {
|
||||
// super.didUpdateWidget(oldWidget);
|
||||
// // Rebuild the marker bitmap if the marker image changes.
|
||||
// if (widget.markerImage != oldWidget.markerImage) {
|
||||
// initializeMarkerBitmap();
|
||||
// setState(() {});
|
||||
// }
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) => AbsorbPointer(
|
||||
// absorbing: !widget.allowInteraction,
|
||||
// child: GoogleMap(
|
||||
// onMapCreated: (controller) async {
|
||||
// _controller.complete(controller);
|
||||
// await controller.setMapStyle(googleMapStyleStrings[widget.style]);
|
||||
// },
|
||||
// onCameraIdle: onCameraIdle,
|
||||
// onCameraMove: (position) => currentMapCenter = position.target,
|
||||
// initialCameraPosition: CameraPosition(
|
||||
// target: initialPosition,
|
||||
// zoom: initialZoom,
|
||||
// ),
|
||||
// mapType: widget.mapType,
|
||||
// zoomGesturesEnabled: widget.allowZoom,
|
||||
// zoomControlsEnabled: widget.showZoomControls,
|
||||
// myLocationEnabled: widget.showLocation,
|
||||
// compassEnabled: widget.showCompass,
|
||||
// mapToolbarEnabled: widget.showMapToolbar,
|
||||
// trafficEnabled: widget.showTraffic,
|
||||
// markers: widget.markers
|
||||
// .map(
|
||||
// (m) => Marker(
|
||||
// markerId: MarkerId(m.markerId),
|
||||
// position: m.location.toGoogleMaps(),
|
||||
// icon: _markerDescriptor ?? BitmapDescriptor.defaultMarker,
|
||||
// onTap: () async {
|
||||
// if (widget.centerMapOnMarkerTap) {
|
||||
// final controller = await _controller.future;
|
||||
// await controller.animateCamera(
|
||||
// CameraUpdate.newLatLng(m.location.toGoogleMaps()),
|
||||
// );
|
||||
// currentMapCenter = m.location.toGoogleMaps();
|
||||
// onCameraIdle();
|
||||
// }
|
||||
// await m.onTap?.call();
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
// .toSet(),
|
||||
// ));
|
||||
// }
|
||||
|
||||
// extension ToGoogleMapsLatLng on latlng.LatLng {
|
||||
// LatLng toGoogleMaps() => LatLng(latitude, longitude);
|
||||
// }
|
||||
|
||||
// extension GoogleMapsToLatLng on LatLng {
|
||||
// latlng.LatLng toLatLng() => latlng.LatLng(latitude, longitude);
|
||||
// }
|
||||
|
||||
// Map<GoogleMapStyle, String> googleMapStyleStrings = {
|
||||
// GoogleMapStyle.standard: '[]',
|
||||
// GoogleMapStyle.silver:
|
||||
// r'[{"elementType":"geometry","stylers":[{"color":"#f5f5f5"}]},{"elementType":"labels.icon","stylers":[{"visibility":"off"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#f5f5f5"}]},{"featureType":"administrative.land_parcel","elementType":"labels.text.fill","stylers":[{"color":"#bdbdbd"}]},{"featureType":"poi","elementType":"geometry","stylers":[{"color":"#eeeeee"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#e5e5e5"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#ffffff"}]},{"featureType":"road.arterial","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#dadada"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"featureType":"road.local","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]},{"featureType":"transit.line","elementType":"geometry","stylers":[{"color":"#e5e5e5"}]},{"featureType":"transit.station","elementType":"geometry","stylers":[{"color":"#eeeeee"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#c9c9c9"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]}]',
|
||||
// GoogleMapStyle.retro:
|
||||
// r'[{"elementType":"geometry","stylers":[{"color":"#ebe3cd"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#523735"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#f5f1e6"}]},{"featureType":"administrative","elementType":"geometry.stroke","stylers":[{"color":"#c9b2a6"}]},{"featureType":"administrative.land_parcel","elementType":"geometry.stroke","stylers":[{"color":"#dcd2be"}]},{"featureType":"administrative.land_parcel","elementType":"labels.text.fill","stylers":[{"color":"#ae9e90"}]},{"featureType":"landscape.natural","elementType":"geometry","stylers":[{"color":"#dfd2ae"}]},{"featureType":"poi","elementType":"geometry","stylers":[{"color":"#dfd2ae"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#93817c"}]},{"featureType":"poi.park","elementType":"geometry.fill","stylers":[{"color":"#a5b076"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#447530"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#f5f1e6"}]},{"featureType":"road.arterial","elementType":"geometry","stylers":[{"color":"#fdfcf8"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#f8c967"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#e9bc62"}]},{"featureType":"road.highway.controlled_access","elementType":"geometry","stylers":[{"color":"#e98d58"}]},{"featureType":"road.highway.controlled_access","elementType":"geometry.stroke","stylers":[{"color":"#db8555"}]},{"featureType":"road.local","elementType":"labels.text.fill","stylers":[{"color":"#806b63"}]},{"featureType":"transit.line","elementType":"geometry","stylers":[{"color":"#dfd2ae"}]},{"featureType":"transit.line","elementType":"labels.text.fill","stylers":[{"color":"#8f7d77"}]},{"featureType":"transit.line","elementType":"labels.text.stroke","stylers":[{"color":"#ebe3cd"}]},{"featureType":"transit.station","elementType":"geometry","stylers":[{"color":"#dfd2ae"}]},{"featureType":"water","elementType":"geometry.fill","stylers":[{"color":"#b9d3c2"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#92998d"}]}]',
|
||||
// GoogleMapStyle.dark:
|
||||
// r'[{"elementType":"geometry","stylers":[{"color":"#212121"}]},{"elementType":"labels.icon","stylers":[{"visibility":"off"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#212121"}]},{"featureType":"administrative","elementType":"geometry","stylers":[{"color":"#757575"}]},{"featureType":"administrative.country","elementType":"labels.text.fill","stylers":[{"color":"#9e9e9e"}]},{"featureType":"administrative.land_parcel","stylers":[{"visibility":"off"}]},{"featureType":"administrative.locality","elementType":"labels.text.fill","stylers":[{"color":"#bdbdbd"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#181818"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"featureType":"poi.park","elementType":"labels.text.stroke","stylers":[{"color":"#1b1b1b"}]},{"featureType":"road","elementType":"geometry.fill","stylers":[{"color":"#2c2c2c"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#8a8a8a"}]},{"featureType":"road.arterial","elementType":"geometry","stylers":[{"color":"#373737"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#3c3c3c"}]},{"featureType":"road.highway.controlled_access","elementType":"geometry","stylers":[{"color":"#4e4e4e"}]},{"featureType":"road.local","elementType":"labels.text.fill","stylers":[{"color":"#616161"}]},{"featureType":"transit","elementType":"labels.text.fill","stylers":[{"color":"#757575"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#000000"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#3d3d3d"}]}]',
|
||||
// GoogleMapStyle.night:
|
||||
// r'[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#746855"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#242f3e"}]},{"featureType":"administrative.locality","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#263c3f"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#6b9a76"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#38414e"}]},{"featureType":"road","elementType":"geometry.stroke","stylers":[{"color":"#212a37"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#9ca5b3"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#746855"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#1f2835"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#f3d19c"}]},{"featureType":"transit","elementType":"geometry","stylers":[{"color":"#2f3948"}]},{"featureType":"transit.station","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#17263c"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#515c6d"}]},{"featureType":"water","elementType":"labels.text.stroke","stylers":[{"color":"#17263c"}]}]',
|
||||
// GoogleMapStyle.aubergine:
|
||||
// r'[{"elementType":"geometry","stylers":[{"color":"#1d2c4d"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#8ec3b9"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#1a3646"}]},{"featureType":"administrative.country","elementType":"geometry.stroke","stylers":[{"color":"#4b6878"}]},{"featureType":"administrative.land_parcel","elementType":"labels.text.fill","stylers":[{"color":"#64779e"}]},{"featureType":"administrative.province","elementType":"geometry.stroke","stylers":[{"color":"#4b6878"}]},{"featureType":"landscape.man_made","elementType":"geometry.stroke","stylers":[{"color":"#334e87"}]},{"featureType":"landscape.natural","elementType":"geometry","stylers":[{"color":"#023e58"}]},{"featureType":"poi","elementType":"geometry","stylers":[{"color":"#283d6a"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#6f9ba5"}]},{"featureType":"poi","elementType":"labels.text.stroke","stylers":[{"color":"#1d2c4d"}]},{"featureType":"poi.park","elementType":"geometry.fill","stylers":[{"color":"#023e58"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#3C7680"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#304a7d"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#98a5be"}]},{"featureType":"road","elementType":"labels.text.stroke","stylers":[{"color":"#1d2c4d"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#2c6675"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#255763"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#b0d5ce"}]},{"featureType":"road.highway","elementType":"labels.text.stroke","stylers":[{"color":"#023e58"}]},{"featureType":"transit","elementType":"labels.text.fill","stylers":[{"color":"#98a5be"}]},{"featureType":"transit","elementType":"labels.text.stroke","stylers":[{"color":"#1d2c4d"}]},{"featureType":"transit.line","elementType":"geometry.fill","stylers":[{"color":"#283d6a"}]},{"featureType":"transit.station","elementType":"geometry","stylers":[{"color":"#3a4762"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#0e1626"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#4e6d70"}]}]',
|
||||
// };
|
||||
|
||||
// Map<GoogleMarkerColor, double> googleMarkerColorMap = {
|
||||
// GoogleMarkerColor.red: 0.0,
|
||||
// GoogleMarkerColor.orange: 30.0,
|
||||
// GoogleMarkerColor.yellow: 60.0,
|
||||
// GoogleMarkerColor.green: 120.0,
|
||||
// GoogleMarkerColor.cyan: 180.0,
|
||||
// GoogleMarkerColor.azure: 210.0,
|
||||
// GoogleMarkerColor.blue: 240.0,
|
||||
// GoogleMarkerColor.violet: 270.0,
|
||||
// GoogleMarkerColor.magenta: 300.0,
|
||||
// GoogleMarkerColor.rose: 330.0,
|
||||
// };
|
||||
185
lib/src/widgets/flutter_flow_icon_button.dart
Normal file
185
lib/src/widgets/flutter_flow_icon_button.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
/// A customizable icon button widget.
|
||||
class FlutterFlowIconButton extends StatefulWidget {
|
||||
/// Creates a [FlutterFlowIconButton].
|
||||
///
|
||||
/// - [icon] parameter is required and specifies the widget to be used as the icon.
|
||||
/// - [borderRadius] parameter specifies the border radius of the button.
|
||||
/// - [buttonSize] parameter specifies the size of the button.
|
||||
/// - [fillColor] parameter specifies the fill color of the button.
|
||||
/// - [disabledColor] parameter specifies the color of the button when it is disabled.
|
||||
/// - [disabledIconColor] parameter specifies the color of the icon when the button is disabled.
|
||||
/// - [hoverColor] parameter specifies the color of the button when it is hovered.
|
||||
/// - [hoverIconColor] parameter specifies the color of the icon when the button is hovered.
|
||||
/// - [borderColor] parameter specifies the border color of the button.
|
||||
/// - [borderWidth] parameter specifies the width of the button's border.
|
||||
/// - [showLoadingIndicator] parameter specifies whether to show a loading indicator on the button.
|
||||
/// - [onPressed] parameter specifies the callback function to be called when the button is pressed.
|
||||
const FlutterFlowIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.borderColor,
|
||||
this.borderRadius,
|
||||
this.borderWidth,
|
||||
this.buttonSize,
|
||||
this.fillColor,
|
||||
this.disabledColor,
|
||||
this.disabledIconColor,
|
||||
this.hoverColor,
|
||||
this.hoverIconColor,
|
||||
this.onPressed,
|
||||
this.showLoadingIndicator = false,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
final double? borderRadius;
|
||||
final double? buttonSize;
|
||||
final Color? fillColor;
|
||||
final Color? disabledColor;
|
||||
final Color? disabledIconColor;
|
||||
final Color? hoverColor;
|
||||
final Color? hoverIconColor;
|
||||
final Color? borderColor;
|
||||
final double? borderWidth;
|
||||
final bool showLoadingIndicator;
|
||||
final Function()? onPressed;
|
||||
|
||||
@override
|
||||
State<FlutterFlowIconButton> createState() => _FlutterFlowIconButtonState();
|
||||
}
|
||||
|
||||
class _FlutterFlowIconButtonState extends State<FlutterFlowIconButton> {
|
||||
bool loading = false;
|
||||
late double? iconSize;
|
||||
late Color? iconColor;
|
||||
late Widget effectiveIcon;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateIcon();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FlutterFlowIconButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_updateIcon();
|
||||
}
|
||||
|
||||
void _updateIcon() {
|
||||
final isFontAwesome = widget.icon is FaIcon;
|
||||
if (isFontAwesome) {
|
||||
FaIcon icon = widget.icon as FaIcon;
|
||||
effectiveIcon = FaIcon(
|
||||
icon.icon,
|
||||
size: icon.size,
|
||||
);
|
||||
iconSize = icon.size;
|
||||
iconColor = icon.color;
|
||||
} else {
|
||||
Icon icon = widget.icon as Icon;
|
||||
effectiveIcon = Icon(
|
||||
icon.icon,
|
||||
size: icon.size,
|
||||
);
|
||||
iconSize = icon.size;
|
||||
iconColor = icon.color;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ButtonStyle style = ButtonStyle(
|
||||
shape: WidgetStateProperty.resolveWith<OutlinedBorder>(
|
||||
(states) {
|
||||
return RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
|
||||
side: BorderSide(
|
||||
color: widget.borderColor ?? Colors.transparent,
|
||||
width: widget.borderWidth ?? 0,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
iconColor: WidgetStateProperty.resolveWith<Color?>(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.disabled) &&
|
||||
widget.disabledIconColor != null) {
|
||||
return widget.disabledIconColor;
|
||||
}
|
||||
if (states.contains(WidgetState.hovered) &&
|
||||
widget.hoverIconColor != null) {
|
||||
return widget.hoverIconColor;
|
||||
}
|
||||
return iconColor;
|
||||
},
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.resolveWith<Color?>(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.disabled) &&
|
||||
widget.disabledColor != null) {
|
||||
return widget.disabledColor;
|
||||
}
|
||||
if (states.contains(WidgetState.hovered) &&
|
||||
widget.hoverColor != null) {
|
||||
return widget.hoverColor;
|
||||
}
|
||||
|
||||
return widget.fillColor;
|
||||
},
|
||||
),
|
||||
overlayColor: WidgetStateProperty.resolveWith<Color?>((states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return null;
|
||||
}
|
||||
return widget.hoverColor == null ? null : Colors.transparent;
|
||||
}),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
width: widget.buttonSize,
|
||||
height: widget.buttonSize,
|
||||
child: Theme(
|
||||
data: ThemeData.from(
|
||||
colorScheme: Theme.of(context).colorScheme,
|
||||
useMaterial3: true,
|
||||
),
|
||||
child: IgnorePointer(
|
||||
ignoring: widget.showLoadingIndicator && loading,
|
||||
child: IconButton(
|
||||
icon: (widget.showLoadingIndicator && loading)
|
||||
? SizedBox(
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
iconColor ?? Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: effectiveIcon,
|
||||
onPressed: widget.onPressed == null
|
||||
? null
|
||||
: () async {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setState(() => loading = true);
|
||||
try {
|
||||
await widget.onPressed!();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
},
|
||||
splashRadius: widget.buttonSize,
|
||||
style: style,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
606
lib/src/widgets/flutter_flow_language_selector.dart
Normal file
606
lib/src/widgets/flutter_flow_language_selector.dart
Normal file
@@ -0,0 +1,606 @@
|
||||
/*
|
||||
* Copyright (c) 2019 gomgom. https://www.gomgom.net
|
||||
*
|
||||
* Source code has been modified by FlutterFlow, Inc. and the below license
|
||||
* applies only to this file. Adapted from "language_picker" pub.dev package.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import 'package:emoji_flag_converter/emoji_flag_converter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FlutterFlowLanguageSelector extends StatelessWidget {
|
||||
const FlutterFlowLanguageSelector({
|
||||
super.key,
|
||||
required this.currentLanguage,
|
||||
required this.languages,
|
||||
required this.onChanged,
|
||||
this.width,
|
||||
this.height,
|
||||
this.backgroundColor,
|
||||
this.borderColor = const Color(0xFF262D34),
|
||||
this.borderRadius = 8.0,
|
||||
this.textStyle,
|
||||
this.hideFlags = false,
|
||||
this.flagSize = 24.0,
|
||||
this.flagTextGap = 8.0,
|
||||
this.dropdownColor,
|
||||
this.dropdownIconColor = const Color(0xFF14181B),
|
||||
this.dropdownIcon,
|
||||
});
|
||||
|
||||
final double? width;
|
||||
final double? height;
|
||||
final String currentLanguage;
|
||||
final List<String> languages;
|
||||
final Function(String) onChanged;
|
||||
final Color? backgroundColor;
|
||||
final Color? borderColor;
|
||||
final double borderRadius;
|
||||
final TextStyle? textStyle;
|
||||
final bool hideFlags;
|
||||
final double flagSize;
|
||||
final double? flagTextGap;
|
||||
final Color? dropdownColor;
|
||||
final Color? dropdownIconColor;
|
||||
final IconData? dropdownIcon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: _LanguagePickerDropdown(
|
||||
currentLanguage: currentLanguage,
|
||||
languages: _languageMap(languages.toSet()),
|
||||
onChanged: onChanged,
|
||||
backgroundColor: backgroundColor,
|
||||
borderColor: borderColor,
|
||||
borderRadius: borderRadius,
|
||||
dropdownColor: dropdownColor,
|
||||
dropdownIconColor: dropdownIconColor,
|
||||
dropdownIcon: dropdownIcon,
|
||||
itemBuilder: (language) => _LanguagePickerItem(
|
||||
language: language.isoCode,
|
||||
languages: languages,
|
||||
textStyle: textStyle,
|
||||
hideFlags: hideFlags,
|
||||
flagSize: flagSize,
|
||||
flagTextGap: flagTextGap,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _LanguagePickerItem extends StatelessWidget {
|
||||
const _LanguagePickerItem({
|
||||
required this.language,
|
||||
required this.languages,
|
||||
this.textStyle,
|
||||
this.hideFlags = false,
|
||||
this.flagSize = 24.0,
|
||||
this.flagTextGap = 8.0,
|
||||
});
|
||||
|
||||
final String language;
|
||||
final List<String> languages;
|
||||
final TextStyle? textStyle;
|
||||
final bool hideFlags;
|
||||
final double flagSize;
|
||||
final double? flagTextGap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final flagInfo = languageToCountryInfo[language];
|
||||
Widget flagWidget = Container();
|
||||
if (flagInfo is String) {
|
||||
final flagEmoji = EmojiConverter.fromAlpha2CountryCode(flagInfo);
|
||||
flagWidget = Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
child: Text(
|
||||
flagEmoji,
|
||||
style: const TextStyle(fontSize: 20.0),
|
||||
),
|
||||
);
|
||||
} else if (flagInfo is Map) {
|
||||
final flagUrl = flagInfo['flag'] as String;
|
||||
flagWidget = Image.network(
|
||||
flagUrl,
|
||||
width: 24,
|
||||
height: 20,
|
||||
);
|
||||
}
|
||||
flagWidget = Transform.scale(
|
||||
scale: flagSize / 24.0,
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
child: flagWidget,
|
||||
),
|
||||
);
|
||||
return Row(
|
||||
children: [
|
||||
if (!hideFlags) ...[
|
||||
flagWidget,
|
||||
SizedBox(width: flagTextGap),
|
||||
],
|
||||
Text(
|
||||
_languageMap(languages.toSet())[language]?.name ?? '',
|
||||
style: textStyle ??
|
||||
const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a customizable [DropdownButton] for all languages
|
||||
class _LanguagePickerDropdown extends StatelessWidget {
|
||||
const _LanguagePickerDropdown({
|
||||
required this.itemBuilder,
|
||||
required this.currentLanguage,
|
||||
required this.onChanged,
|
||||
required this.languages,
|
||||
this.backgroundColor,
|
||||
this.borderColor = const Color(0xFF262D34),
|
||||
this.borderRadius = 8.0,
|
||||
this.dropdownColor,
|
||||
this.dropdownIconColor = const Color(0xFF14181B),
|
||||
this.dropdownIcon,
|
||||
});
|
||||
|
||||
/// This function will be called to build the child of DropdownMenuItem.
|
||||
final Widget Function(Language) itemBuilder;
|
||||
|
||||
/// The current ISO ALPHA-2 code.
|
||||
final String currentLanguage;
|
||||
|
||||
/// This function will be called whenever a Language item is selected.
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
/// List of languages available in this picker.
|
||||
final Map<String, Language> languages;
|
||||
|
||||
final Color? backgroundColor;
|
||||
final Color? borderColor;
|
||||
final double borderRadius;
|
||||
final Color? dropdownColor;
|
||||
final Color? dropdownIconColor;
|
||||
final IconData? dropdownIcon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<DropdownMenuItem<String>> items = languages.values
|
||||
.map(
|
||||
(language) => DropdownMenuItem<String>(
|
||||
value: language.isoCode,
|
||||
child: itemBuilder(language),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return Container(
|
||||
height: 44.0,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border.all(color: borderColor ?? Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
child: Center(
|
||||
child: DropdownButton<String>(
|
||||
isExpanded: true,
|
||||
underline: Container(),
|
||||
dropdownColor: dropdownColor ?? backgroundColor,
|
||||
focusColor: Colors.transparent,
|
||||
iconEnabledColor: dropdownIconColor,
|
||||
iconDisabledColor: dropdownIconColor,
|
||||
icon: dropdownIcon != null
|
||||
? Icon(
|
||||
dropdownIcon,
|
||||
size: 18.0,
|
||||
color: dropdownIconColor,
|
||||
)
|
||||
: null,
|
||||
hint: const Text(
|
||||
'Unset',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontFamily: 'Product Sans',
|
||||
fontStyle: FontStyle.italic,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
onChanged(val);
|
||||
}
|
||||
},
|
||||
items: items,
|
||||
value: currentLanguage.isNotEmpty ? currentLanguage : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Language {
|
||||
Language(this.isoCode, this.name);
|
||||
|
||||
Language.fromMap(Map<String, String> map)
|
||||
: name = map['name']!,
|
||||
isoCode = map['isoCode']!;
|
||||
|
||||
final String name;
|
||||
final String isoCode;
|
||||
}
|
||||
|
||||
Map<String, Language> _languageMap(Set<String> languages) => Map.fromEntries(
|
||||
_defaultLanguagesList
|
||||
.where((element) => languages.contains(element['isoCode']))
|
||||
.map((e) => MapEntry(e['isoCode']!, Language.fromMap(e))),
|
||||
);
|
||||
|
||||
final List<Map<String, String>> _defaultLanguagesList = [
|
||||
{"isoCode": "aa", "name": "Afaraf"},
|
||||
{"isoCode": "af", "name": "Afrikaans"},
|
||||
{"isoCode": "ak", "name": "Akan"},
|
||||
{"isoCode": "sq", "name": "Shqip"},
|
||||
{"isoCode": "am", "name": "አማርኛ"},
|
||||
{"isoCode": "ar", "name": "العربية"},
|
||||
{"isoCode": "hy", "name": "Հայերեն"},
|
||||
{"isoCode": "as", "name": "অসমীয়া"},
|
||||
{"isoCode": "ay", "name": "aymar"},
|
||||
{"isoCode": "az", "name": "azərbaycan"},
|
||||
{"isoCode": "bm", "name": "bamanankan"},
|
||||
{"isoCode": "ba", "name": "башҡорт теле"},
|
||||
{"isoCode": "eu", "name": "euskara, euskera"},
|
||||
{"isoCode": "be", "name": "беларуская мова"},
|
||||
{"isoCode": "bn", "name": "বাংলা"},
|
||||
{"isoCode": "bh", "name": "भोजपुरी"},
|
||||
{"isoCode": "bi", "name": "Bislama"},
|
||||
{"isoCode": "nb", "name": "Norsk bokmål"},
|
||||
{"isoCode": "bs", "name": "bosanski jezik"},
|
||||
{"isoCode": "br", "name": "brezhoneg"},
|
||||
{"isoCode": "bg", "name": "български език"},
|
||||
{"isoCode": "my", "name": "ဗမာစာ"},
|
||||
{"isoCode": "ca", "name": "català"},
|
||||
{"isoCode": "km", "name": "ភាសាខ្មែរ"},
|
||||
{"isoCode": "ch", "name": "Chamoru"},
|
||||
{"isoCode": "ce", "name": "нохчийн мотт"},
|
||||
{"isoCode": "ny", "name": "chiCheŵa"},
|
||||
{"isoCode": "zh_Hans", "name": "中文 (简体)"},
|
||||
{"isoCode": "zh_Hant", "name": "中文 (繁體)"},
|
||||
{"isoCode": "cv", "name": "чӑваш чӗлхи"},
|
||||
{"isoCode": "cr", "name": "ᓀᐦᐃᔭᐍᐏᐣ"},
|
||||
{"isoCode": "hr", "name": "hrvatski jezik"},
|
||||
{"isoCode": "cs", "name": "čeština"},
|
||||
{"isoCode": "da", "name": "dansk"},
|
||||
{"isoCode": "dv", "name": "ދިވެހި"},
|
||||
{"isoCode": "nl", "name": "Nederlands"},
|
||||
{"isoCode": "dz", "name": "རྫོང་ཁ"},
|
||||
{"isoCode": "en", "name": "English"},
|
||||
{"isoCode": "eo", "name": "Esperanto"},
|
||||
{"isoCode": "et", "name": "eesti"},
|
||||
{"isoCode": "ee", "name": "Eʋegbe"},
|
||||
{"isoCode": "fo", "name": "føroyskt"},
|
||||
{"isoCode": "fj", "name": "vosa Vakaviti"},
|
||||
{"isoCode": "fi", "name": "suomi"},
|
||||
{"isoCode": "fr", "name": "français"},
|
||||
{"isoCode": "ff", "name": "Fulfulde"},
|
||||
{"isoCode": "gd", "name": "Gàidhlig"},
|
||||
{"isoCode": "gl", "name": "galego"},
|
||||
{"isoCode": "lg", "name": "Luganda"},
|
||||
{"isoCode": "ka", "name": "ქართული"},
|
||||
{"isoCode": "de", "name": "Deutsch"},
|
||||
{"isoCode": "el", "name": "ελληνικά"},
|
||||
{"isoCode": "gn", "name": "Avañe'ẽ"},
|
||||
{"isoCode": "gu", "name": "ગુજરાતી"},
|
||||
{"isoCode": "ht", "name": "Kreyòl ayisyen"},
|
||||
{"isoCode": "ha", "name": "هَوُسَ"},
|
||||
{"isoCode": "he", "name": "עברית"},
|
||||
{"isoCode": "hz", "name": "Otjiherero"},
|
||||
{"isoCode": "hi", "name": "हिन्दी, हिंदी"},
|
||||
{"isoCode": "ho", "name": "Hiri Motu"},
|
||||
{"isoCode": "hu", "name": "magyar"},
|
||||
{"isoCode": "is", "name": "Íslenska"},
|
||||
{"isoCode": "io", "name": "Ido"},
|
||||
{"isoCode": "ig", "name": "Asụsụ Igbo"},
|
||||
{"isoCode": "id", "name": "Bahasa Indonesia"},
|
||||
{"isoCode": "ia", "name": "Interlingua"},
|
||||
{"isoCode": "ie", "name": "Interlingue"},
|
||||
{"isoCode": "iu", "name": "ᐃᓄᒃᑎᑐᑦ"},
|
||||
{"isoCode": "ik", "name": "Iñupiaq"},
|
||||
{"isoCode": "ga", "name": "Gaeilge"},
|
||||
{"isoCode": "it", "name": "Italiano"},
|
||||
{"isoCode": "ja", "name": "日本語 (にほんご)"},
|
||||
{"isoCode": "jv", "name": "ꦧꦱꦗꦮ"},
|
||||
{"isoCode": "kl", "name": "kalaallisut"},
|
||||
{"isoCode": "kn", "name": "ಕನ್ನಡ"},
|
||||
{"isoCode": "kr", "name": "Kanuri"},
|
||||
{"isoCode": "ks", "name": "कश्मीरी"},
|
||||
{"isoCode": "kk", "name": "қазақ тілі"},
|
||||
{"isoCode": "ki", "name": "Gĩkũyũ"},
|
||||
{"isoCode": "rw", "name": "Ikinyarwanda"},
|
||||
{"isoCode": "ky", "name": "Кыргызча"},
|
||||
{"isoCode": "kv", "name": "коми кыв"},
|
||||
{"isoCode": "kg", "name": "Kikongo"},
|
||||
{"isoCode": "ko", "name": "한국어"},
|
||||
{"isoCode": "kj", "name": "Kuanyama"},
|
||||
{"isoCode": "ku", "name": "Kurdî"},
|
||||
{"isoCode": "lo", "name": "ພາສາລາວ"},
|
||||
{"isoCode": "la", "name": "latine"},
|
||||
{"isoCode": "lv", "name": "latviešu valoda"},
|
||||
{"isoCode": "li", "name": "Limburgs"},
|
||||
{"isoCode": "ln", "name": "Lingála"},
|
||||
{"isoCode": "lt", "name": "lietuvių kalba"},
|
||||
{"isoCode": "lu", "name": "Tshiluba"},
|
||||
{"isoCode": "lb", "name": "Lëtzebuergesch"},
|
||||
{"isoCode": "mk", "name": "македонски јазик"},
|
||||
{"isoCode": "mg", "name": "fiteny malagasy"},
|
||||
{"isoCode": "ms", "name": "bahasa Melayu"},
|
||||
{"isoCode": "ml", "name": "മലയാളം"},
|
||||
{"isoCode": "mt", "name": "Malti"},
|
||||
{"isoCode": "gv", "name": "Gaelg, Gailck"},
|
||||
{"isoCode": "mi", "name": "te reo Māori"},
|
||||
{"isoCode": "mr", "name": "मराठी"},
|
||||
{"isoCode": "mh", "name": "Kajin M̧ajeļ"},
|
||||
{"isoCode": "mn", "name": "Монгол хэл"},
|
||||
{"isoCode": "na", "name": "Dorerin Naoero"},
|
||||
{"isoCode": "nv", "name": "Diné bizaad"},
|
||||
{"isoCode": "nd", "name": "Ndebele (Southern)"},
|
||||
{"isoCode": "nr", "name": "Ndebele (Northern)"},
|
||||
{"isoCode": "ng", "name": "Owambo"},
|
||||
{"isoCode": "ne", "name": "नेपाली"},
|
||||
{"isoCode": "se", "name": "Davvisámegiella"},
|
||||
{"isoCode": "no", "name": "Norsk"},
|
||||
{"isoCode": "nn", "name": "Norsk nynorsk"},
|
||||
{"isoCode": "oc", "name": "occitan"},
|
||||
{"isoCode": "oj", "name": "ᐊᓂᔑᓈᐯᒧᐎᓐ"},
|
||||
{"isoCode": "or", "name": "ଓଡ଼ିଆ"},
|
||||
{"isoCode": "om", "name": "Afaan Oromoo"},
|
||||
{"isoCode": "os", "name": "ирон æвзаг"},
|
||||
{"isoCode": "pi", "name": "पाऴि"},
|
||||
{"isoCode": "pa", "name": "ਪੰਜਾਬੀ"},
|
||||
{"isoCode": "fa", "name": "فارسی"},
|
||||
{"isoCode": "pl", "name": "Polski"},
|
||||
{"isoCode": "pt", "name": "Português"},
|
||||
{"isoCode": "ps", "name": "پښتو"},
|
||||
{"isoCode": "qu", "name": "Runa Simi, Kichwa"},
|
||||
{"isoCode": "ro", "name": "Română"},
|
||||
{"isoCode": "rm", "name": "rumantsch grischun"},
|
||||
{"isoCode": "rn", "name": "Ikirundi"},
|
||||
{"isoCode": "ru", "name": "Русский"},
|
||||
{"isoCode": "sm", "name": "gagana fa'a Samoa"},
|
||||
{"isoCode": "sg", "name": "yângâ tî sängö"},
|
||||
{"isoCode": "sa", "name": "संस्कृतम्"},
|
||||
{"isoCode": "sc", "name": "sardu"},
|
||||
{"isoCode": "sr", "name": "српски језик"},
|
||||
{"isoCode": "sn", "name": "chiShona"},
|
||||
{"isoCode": "ii", "name": "ꆈꌠ꒿ Nuosuhxop"},
|
||||
{"isoCode": "sd", "name": "सिन्धी"},
|
||||
{"isoCode": "si", "name": "සිංහල"},
|
||||
{"isoCode": "sk", "name": "slovenský jazyk"},
|
||||
{"isoCode": "sl", "name": "slovenščina"},
|
||||
{"isoCode": "so", "name": "Soomaaliga"},
|
||||
{"isoCode": "st", "name": "Sesotho"},
|
||||
{"isoCode": "es", "name": "Español"},
|
||||
{"isoCode": "su", "name": "Basa Sunda"},
|
||||
{"isoCode": "sw", "name": "Kiswahili"},
|
||||
{"isoCode": "ss", "name": "SiSwati"},
|
||||
{"isoCode": "sv", "name": "svenska"},
|
||||
{"isoCode": "tl", "name": "Tagalog"},
|
||||
{"isoCode": "ty", "name": "Reo Tahiti"},
|
||||
{"isoCode": "tg", "name": "тоҷикӣ"},
|
||||
{"isoCode": "ta", "name": "தமிழ்"},
|
||||
{"isoCode": "tt", "name": "татар теле"},
|
||||
{"isoCode": "te", "name": "తెలుగు"},
|
||||
{"isoCode": "th", "name": "ไทย"},
|
||||
{"isoCode": "bo", "name": "བོད་ཡིག"},
|
||||
{"isoCode": "ti", "name": "ትግርኛ"},
|
||||
{"isoCode": "to", "name": "faka Tonga"},
|
||||
{"isoCode": "ts", "name": "Xitsonga"},
|
||||
{"isoCode": "tn", "name": "Setswana"},
|
||||
{"isoCode": "tr", "name": "Türkçe"},
|
||||
{"isoCode": "tk", "name": "Түркмен"},
|
||||
{"isoCode": "tw", "name": "Twi"},
|
||||
{"isoCode": "ug", "name": "ئۇيغۇرچە"},
|
||||
{"isoCode": "uk", "name": "Українська"},
|
||||
{"isoCode": "ur", "name": "اردو"},
|
||||
{"isoCode": "uz", "name": "Oʻzbek"},
|
||||
{"isoCode": "ve", "name": "Tshivenḓa"},
|
||||
{"isoCode": "vi", "name": "Tiếng Việt"},
|
||||
{"isoCode": "vo", "name": "Volapük"},
|
||||
{"isoCode": "wa", "name": "walon"},
|
||||
{"isoCode": "cy", "name": "Cymraeg"},
|
||||
{"isoCode": "fy", "name": "Frysk"},
|
||||
{"isoCode": "wo", "name": "Wollof"},
|
||||
{"isoCode": "xh", "name": "Xhosa"},
|
||||
{"isoCode": "yi", "name": "ייִדיש"},
|
||||
{"isoCode": "yo", "name": "Yorùbá"},
|
||||
{"isoCode": "za", "name": "Saɯ cueŋƅ"},
|
||||
{"isoCode": "zu", "name": "Zulu"},
|
||||
];
|
||||
|
||||
final Map<String, dynamic> languageToCountryInfo = {
|
||||
"aa": "dj",
|
||||
"af": "za",
|
||||
"ak": "gh",
|
||||
"sq": "al",
|
||||
"am": "et",
|
||||
"ar": {
|
||||
"proposed_iso_3166": "aa",
|
||||
"flag":
|
||||
"https://upload.wikimedia.org/wikipedia/commons/thumb/2/2b/Flag_of_the_Arab_League.svg/400px-Flag_of_the_Arab_League.svg.png",
|
||||
"name": "Arab League"
|
||||
},
|
||||
"hy": "am",
|
||||
"ay": {
|
||||
"proposed_iso_3166": "wh",
|
||||
"flag":
|
||||
"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Banner_of_the_Qulla_Suyu.svg/1920px-Banner_of_the_Qulla_Suyu.svg.png",
|
||||
"name": "Wiphala"
|
||||
},
|
||||
"az": "az",
|
||||
"bm": "ml",
|
||||
"be": "by",
|
||||
"bn": "bd",
|
||||
"bi": "vu",
|
||||
"bs": "ba",
|
||||
"bg": "bg",
|
||||
"my": "mm",
|
||||
"ca": "ad",
|
||||
"zh": "cn",
|
||||
"hr": "hr",
|
||||
"cs": "cz",
|
||||
"da": "dk",
|
||||
"dv": "mv",
|
||||
"nl": "nl",
|
||||
"dz": "bt",
|
||||
"en": "gb",
|
||||
"et": "ee",
|
||||
"ee": {
|
||||
"proposed_iso_3166": "ew",
|
||||
"flag":
|
||||
"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/Flag_of_the_Ewe_people.svg/2880px-Flag_of_the_Ewe_people.svg.png",
|
||||
"name": "Ewe"
|
||||
},
|
||||
"fj": "fj",
|
||||
"fil": "ph",
|
||||
"fi": "fi",
|
||||
"fr": "fr",
|
||||
"gaa": "gh",
|
||||
"ka": "ge",
|
||||
"kl": "gl",
|
||||
"de": "de",
|
||||
"el": "gr",
|
||||
"gu": "in",
|
||||
"ht": "ht",
|
||||
"he": "il",
|
||||
"hi": "in",
|
||||
"ho": "pg",
|
||||
"hu": "hu",
|
||||
"is": "is",
|
||||
"ig": "ng",
|
||||
"id": "id",
|
||||
"ga": "ie",
|
||||
"it": "it",
|
||||
"ja": "jp",
|
||||
"kr": "ne",
|
||||
"kk": "kz",
|
||||
"km": "kh",
|
||||
"kmb": "ao",
|
||||
"rw": "rw",
|
||||
"kg": "cg",
|
||||
"ko": "kr",
|
||||
"kj": "ao",
|
||||
"ku": "iq",
|
||||
"ky": "kg",
|
||||
"lo": "la",
|
||||
"la": "va",
|
||||
"lv": "lv",
|
||||
"ln": "cg",
|
||||
"lt": "lt",
|
||||
"lu": "cd",
|
||||
"lb": "lu",
|
||||
"mk": "mk",
|
||||
"mg": "mg",
|
||||
"ms": "my",
|
||||
"mt": "mt",
|
||||
"mi": "nz",
|
||||
"mh": "mh",
|
||||
"mn": "mn",
|
||||
"mos": "bf",
|
||||
"ne": "np",
|
||||
"nd": "zw",
|
||||
"nso": "za",
|
||||
"no": "no",
|
||||
"nb": "no",
|
||||
"nn": "no",
|
||||
"ny": "mw",
|
||||
"pap": "aw",
|
||||
"ps": "af",
|
||||
"fa": "ir",
|
||||
"pl": "pl",
|
||||
"pt": "pt",
|
||||
"pa": "in",
|
||||
"qu": "wh",
|
||||
"ro": "ro",
|
||||
"rm": "ch",
|
||||
"rn": "bi",
|
||||
"ru": "ru",
|
||||
"sg": "cf",
|
||||
"sr": "rs",
|
||||
"srr": "sn",
|
||||
"sn": "zw",
|
||||
"si": "lk",
|
||||
"sk": "sk",
|
||||
"sl": "si",
|
||||
"so": "so",
|
||||
"snk": "sn",
|
||||
"nr": "za",
|
||||
"st": "ls",
|
||||
"es": "es",
|
||||
"sw": {
|
||||
"proposed_iso_3166": "sw",
|
||||
"flag":
|
||||
"https://upload.wikimedia.org/wikipedia/commons/d/de/Flag_of_Swahili.gif",
|
||||
"name": "Swahili"
|
||||
},
|
||||
"ss": "sz",
|
||||
"sv": "se",
|
||||
"tl": "ph",
|
||||
"tg": "tj",
|
||||
"ta": "lk",
|
||||
"te": "in",
|
||||
"tet": "tl",
|
||||
"th": "th",
|
||||
"ti": "er",
|
||||
"tpi": "pg",
|
||||
"ts": "za",
|
||||
"tn": "bw",
|
||||
"tr": "tr",
|
||||
"tk": "tm",
|
||||
"uk": "ua",
|
||||
"umb": "ao",
|
||||
"ur": "pk",
|
||||
"uz": "uz",
|
||||
"ve": "za",
|
||||
"vi": "vn",
|
||||
"cy": "gb",
|
||||
"wo": "sn",
|
||||
"xh": "za",
|
||||
"yo": {
|
||||
"proposed_iso_3166": "yo",
|
||||
"flag":
|
||||
"https://upload.wikimedia.org/wikipedia/commons/0/04/Flag_of_the_Yoruba_people.svg",
|
||||
"name": "Yoruba"
|
||||
},
|
||||
"zu": "za",
|
||||
// Custom
|
||||
"zh_Hans": "cn",
|
||||
"zh_Hant": "cn",
|
||||
"fo": "fo",
|
||||
"bo": "bo",
|
||||
"to": "to",
|
||||
};
|
||||
29
lib/src/widgets/flutter_flow_media_display.dart
Normal file
29
lib/src/widgets/flutter_flow_media_display.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mime_type/mime_type.dart';
|
||||
|
||||
const _kSupportedVideoMimes = {'video/mp4', 'video/mpeg'};
|
||||
|
||||
bool _isVideoPath(String path) =>
|
||||
_kSupportedVideoMimes.contains(mime(path.split('?').first));
|
||||
|
||||
class FlutterFlowMediaDisplay extends StatelessWidget {
|
||||
/// Creates a [FlutterFlowMediaDisplay] widget.
|
||||
///
|
||||
/// - [path] parameter specifies the path of the media content.
|
||||
/// - [imageBuilder] parameter is a function that takes a [String] path and returns a widget to display an image.
|
||||
/// - [videoPlayerBuilder] parameter is a function that takes a [String] path and returns a widget to display a video player.
|
||||
const FlutterFlowMediaDisplay({
|
||||
super.key,
|
||||
required this.path,
|
||||
required this.imageBuilder,
|
||||
required this.videoPlayerBuilder,
|
||||
});
|
||||
|
||||
final String path;
|
||||
final Widget Function(String) imageBuilder;
|
||||
final Widget Function(String) videoPlayerBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
_isVideoPath(path) ? videoPlayerBuilder(path) : imageBuilder(path);
|
||||
}
|
||||
261
lib/src/widgets/flutter_flow_mux_broadcast.dart
Normal file
261
lib/src/widgets/flutter_flow_mux_broadcast.dart
Normal file
@@ -0,0 +1,261 @@
|
||||
import 'package:apivideo_live_stream/apivideo_live_stream.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'flutter_flow_widgets.dart';
|
||||
|
||||
/// A widget that helps to create a live stream using the Mux API.
|
||||
class FlutterFlowMuxBroadcast extends StatefulWidget {
|
||||
const FlutterFlowMuxBroadcast({
|
||||
super.key,
|
||||
required this.isCameraInitialized,
|
||||
required this.isStreaming,
|
||||
required this.durationString,
|
||||
this.borderRadius = BorderRadius.zero,
|
||||
required this.controller,
|
||||
required this.videoConfig,
|
||||
required this.onCameraRotateButtonTap,
|
||||
required this.startButtonText,
|
||||
required this.onStartButtonTap,
|
||||
required this.onStopButtonTap,
|
||||
required this.startButtonOptions,
|
||||
required this.startButtonIcon,
|
||||
required this.liveText,
|
||||
required this.liveTextStyle,
|
||||
required this.liveIcon,
|
||||
required this.liveTextBackgroundColor,
|
||||
this.liveContainerBorderRadius = BorderRadius.zero,
|
||||
required this.durationTextStyle,
|
||||
required this.durationTextBackgroundColor,
|
||||
this.durationContainerBorderRadius = BorderRadius.zero,
|
||||
required this.rotateButtonIcon,
|
||||
required this.rotateButtonColor,
|
||||
required this.stopButtonColor,
|
||||
required this.stopButtonIcon,
|
||||
});
|
||||
|
||||
/// Whether the camera is initialized or not.
|
||||
final bool isCameraInitialized;
|
||||
|
||||
/// Whether the video is currently being streamed or not.
|
||||
final bool isStreaming;
|
||||
|
||||
/// The duration of the video stream.
|
||||
final String? durationString;
|
||||
|
||||
/// The border radius of the widget.
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
/// The controller for the live stream.
|
||||
final LiveStreamController? controller;
|
||||
|
||||
/// The configuration for the video stream.
|
||||
final VideoConfig videoConfig;
|
||||
|
||||
/// Callback function when the camera rotate button is tapped.
|
||||
final Function onCameraRotateButtonTap;
|
||||
|
||||
/// The text for the start button.
|
||||
final String startButtonText;
|
||||
|
||||
/// Callback function when the start button is tapped.
|
||||
final Function onStartButtonTap;
|
||||
|
||||
/// Callback function when the stop button is tapped.
|
||||
final Function onStopButtonTap;
|
||||
|
||||
/// The options for the start button.
|
||||
final FFButtonOptions startButtonOptions;
|
||||
|
||||
/// The icon for the start button.
|
||||
final Widget startButtonIcon;
|
||||
|
||||
/// The text for the live indicator.
|
||||
final String liveText;
|
||||
|
||||
/// The style for the live indicator text.
|
||||
final TextStyle liveTextStyle;
|
||||
|
||||
/// The icon for the live indicator.
|
||||
final Widget liveIcon;
|
||||
|
||||
/// The background color for the live indicator.
|
||||
final Color liveTextBackgroundColor;
|
||||
|
||||
/// The border radius for the live indicator container.
|
||||
final BorderRadius liveContainerBorderRadius;
|
||||
|
||||
/// The style for the duration text.
|
||||
final TextStyle durationTextStyle;
|
||||
|
||||
/// The background color for the duration text.
|
||||
final Color durationTextBackgroundColor;
|
||||
|
||||
/// The border radius for the duration text container.
|
||||
final BorderRadius durationContainerBorderRadius;
|
||||
|
||||
/// The icon for the rotate button.
|
||||
final Widget rotateButtonIcon;
|
||||
|
||||
/// The color for the rotate button.
|
||||
final Color rotateButtonColor;
|
||||
|
||||
/// The color for the stop button.
|
||||
final Color stopButtonColor;
|
||||
|
||||
/// The icon for the stop button.
|
||||
final Widget stopButtonIcon;
|
||||
|
||||
@override
|
||||
State<FlutterFlowMuxBroadcast> createState() =>
|
||||
_FlutterFlowMuxBroadcastState();
|
||||
}
|
||||
|
||||
class _FlutterFlowMuxBroadcastState extends State<FlutterFlowMuxBroadcast>
|
||||
with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
widget.controller?.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.inactive) {
|
||||
final isStreaming = widget.controller?.isStreaming ?? false;
|
||||
if (isStreaming) {
|
||||
widget.onStopButtonTap();
|
||||
}
|
||||
widget.controller?.stop();
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
widget.controller?.startPreview();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.isCameraInitialized
|
||||
? ClipRRect(
|
||||
borderRadius: widget.borderRadius,
|
||||
child: CameraPreview(
|
||||
controller: widget.controller!,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
top: 16.0,
|
||||
bottom: 16.0,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: widget.isStreaming
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: InkWell(
|
||||
onTap: () => widget.onStopButtonTap(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: widget.stopButtonColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: widget.stopButtonIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => widget.onCameraRotateButtonTap(),
|
||||
child: CircleAvatar(
|
||||
radius:
|
||||
(widget.rotateButtonIcon as Icon).size,
|
||||
backgroundColor: widget.rotateButtonColor,
|
||||
child: Center(
|
||||
child: widget.rotateButtonIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
FFButtonWidget(
|
||||
onPressed: () => widget.onStartButtonTap(),
|
||||
text: widget.startButtonText,
|
||||
icon: widget.startButtonIcon,
|
||||
options: widget.startButtonOptions,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
widget.isStreaming
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: widget.liveTextBackgroundColor,
|
||||
borderRadius:
|
||||
widget.liveContainerBorderRadius,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
widget.liveIcon,
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.liveText,
|
||||
style: widget.liveTextStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: widget.durationTextBackgroundColor,
|
||||
borderRadius:
|
||||
widget.durationContainerBorderRadius,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
widget.durationString ?? '00:00:00',
|
||||
style: widget.durationTextStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
318
lib/src/widgets/flutter_flow_radio_button.dart
Normal file
318
lib/src/widgets/flutter_flow_radio_button.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright 2020 https://github.com/TercyoStorck
|
||||
*
|
||||
* Source code has been modified by FlutterFlow, Inc.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
|
||||
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
||||
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutterflow_ui/src/utils/form_field_controller.dart';
|
||||
|
||||
/// A custom radio button widget that allows the user to select a single option from a list of options.
|
||||
class FlutterFlowRadioButton extends StatefulWidget {
|
||||
/// Creates a [FlutterFlowRadioButton].
|
||||
const FlutterFlowRadioButton({
|
||||
super.key,
|
||||
required this.options,
|
||||
required this.onChanged,
|
||||
required this.controller,
|
||||
required this.optionHeight,
|
||||
required this.textStyle,
|
||||
this.optionWidth,
|
||||
this.selectedTextStyle,
|
||||
this.textPadding = EdgeInsets.zero,
|
||||
this.buttonPosition = RadioButtonPosition.left,
|
||||
this.direction = Axis.vertical,
|
||||
required this.radioButtonColor,
|
||||
this.inactiveRadioButtonColor,
|
||||
this.toggleable = false,
|
||||
this.horizontalAlignment = WrapAlignment.start,
|
||||
this.verticalAlignment = WrapCrossAlignment.start,
|
||||
});
|
||||
|
||||
/// The list of options to choose from.
|
||||
final List<String> options;
|
||||
|
||||
/// A callback function that will be called when the selected option changes.
|
||||
final Function(String?)? onChanged;
|
||||
|
||||
/// A form field controller that manages the state of the selected option.
|
||||
final FormFieldController<String> controller;
|
||||
|
||||
/// The height of each option.
|
||||
final double optionHeight;
|
||||
|
||||
/// The width of each option. If not provided, the width will be determined automatically.
|
||||
final double? optionWidth;
|
||||
|
||||
/// The style of the option text.
|
||||
final TextStyle textStyle;
|
||||
|
||||
/// The style of the selected option text. If not provided, the [textStyle] will be used.
|
||||
final TextStyle? selectedTextStyle;
|
||||
|
||||
/// The padding around the option text.
|
||||
final EdgeInsetsGeometry textPadding;
|
||||
|
||||
/// The position of the radio button relative to the option text.
|
||||
final RadioButtonPosition buttonPosition;
|
||||
|
||||
/// The direction in which the options are laid out.
|
||||
final Axis direction;
|
||||
|
||||
/// The color of the radio button.
|
||||
final Color radioButtonColor;
|
||||
|
||||
/// The color of the radio button when it is not selected. If not provided, the [radioButtonColor] will be used.
|
||||
final Color? inactiveRadioButtonColor;
|
||||
|
||||
/// Whether the radio button can be toggled on and off.
|
||||
final bool toggleable;
|
||||
|
||||
/// The horizontal alignment of the options when the direction is horizontal.
|
||||
final WrapAlignment horizontalAlignment;
|
||||
|
||||
/// The vertical alignment of the options when the direction is vertical.
|
||||
final WrapCrossAlignment verticalAlignment;
|
||||
|
||||
@override
|
||||
State<FlutterFlowRadioButton> createState() => _FlutterFlowRadioButtonState();
|
||||
}
|
||||
|
||||
class _FlutterFlowRadioButtonState extends State<FlutterFlowRadioButton> {
|
||||
bool get enabled => widget.onChanged != null;
|
||||
FormFieldController<String> get controller => widget.controller;
|
||||
void Function()? _listener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_maybeSetOnChangedListener();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_maybeRemoveOnChangedListener();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FlutterFlowRadioButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final oldWidgetEnabled = oldWidget.onChanged != null;
|
||||
if (oldWidgetEnabled != enabled) {
|
||||
_maybeRemoveOnChangedListener();
|
||||
_maybeSetOnChangedListener();
|
||||
}
|
||||
}
|
||||
|
||||
void _maybeSetOnChangedListener() {
|
||||
if (enabled) {
|
||||
_listener = () => widget.onChanged!(controller.value);
|
||||
controller.addListener(_listener!);
|
||||
}
|
||||
}
|
||||
|
||||
void _maybeRemoveOnChangedListener() {
|
||||
if (_listener != null) {
|
||||
controller.removeListener(_listener!);
|
||||
_listener = null;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> get effectiveOptions =>
|
||||
widget.options.isEmpty ? ['[Option]'] : widget.options;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context)
|
||||
.copyWith(unselectedWidgetColor: widget.inactiveRadioButtonColor),
|
||||
child: RadioGroup<String>.builder(
|
||||
direction: widget.direction,
|
||||
groupValue: controller.value,
|
||||
onChanged: enabled ? (value) => controller.value = value : null,
|
||||
activeColor: widget.radioButtonColor,
|
||||
toggleable: widget.toggleable,
|
||||
textStyle: widget.textStyle,
|
||||
selectedTextStyle: widget.selectedTextStyle ?? widget.textStyle,
|
||||
textPadding: widget.textPadding,
|
||||
optionHeight: widget.optionHeight,
|
||||
optionWidth: widget.optionWidth,
|
||||
horizontalAlignment: widget.horizontalAlignment,
|
||||
verticalAlignment: widget.verticalAlignment,
|
||||
items: effectiveOptions,
|
||||
itemBuilder: (item) =>
|
||||
RadioButtonBuilder(item, buttonPosition: widget.buttonPosition),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum RadioButtonPosition {
|
||||
right,
|
||||
left,
|
||||
}
|
||||
|
||||
class RadioButtonBuilder<T> {
|
||||
RadioButtonBuilder(
|
||||
this.description, {
|
||||
this.buttonPosition = RadioButtonPosition.left,
|
||||
});
|
||||
|
||||
final String description;
|
||||
final RadioButtonPosition buttonPosition;
|
||||
}
|
||||
|
||||
class RadioButton<T> extends StatelessWidget {
|
||||
const RadioButton({
|
||||
super.key,
|
||||
required this.description,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
required this.buttonPosition,
|
||||
required this.activeColor,
|
||||
required this.toggleable,
|
||||
required this.textStyle,
|
||||
required this.selectedTextStyle,
|
||||
required this.textPadding,
|
||||
this.shouldFlex = false,
|
||||
});
|
||||
|
||||
final String description;
|
||||
final T value;
|
||||
final T? groupValue;
|
||||
final void Function(T?)? onChanged;
|
||||
final RadioButtonPosition buttonPosition;
|
||||
final Color activeColor;
|
||||
final bool toggleable;
|
||||
final TextStyle textStyle;
|
||||
final TextStyle selectedTextStyle;
|
||||
final EdgeInsetsGeometry textPadding;
|
||||
final bool shouldFlex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedStyle = selectedTextStyle;
|
||||
final isSelected = value == groupValue;
|
||||
Widget radioButtonText = Padding(
|
||||
padding: textPadding,
|
||||
child: Text(
|
||||
description,
|
||||
style: isSelected ? selectedStyle : textStyle,
|
||||
),
|
||||
);
|
||||
if (shouldFlex) {
|
||||
radioButtonText = Flexible(child: radioButtonText);
|
||||
}
|
||||
return InkWell(
|
||||
onTap: onChanged != null ? () => onChanged!(value) : null,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (buttonPosition == RadioButtonPosition.right) radioButtonText,
|
||||
Radio<T>(
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
value: value,
|
||||
activeColor: activeColor,
|
||||
toggleable: toggleable,
|
||||
),
|
||||
if (buttonPosition == RadioButtonPosition.left) radioButtonText,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RadioGroup<T> extends StatelessWidget {
|
||||
const RadioGroup.builder({
|
||||
super.key,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
required this.items,
|
||||
required this.itemBuilder,
|
||||
required this.direction,
|
||||
required this.optionHeight,
|
||||
required this.horizontalAlignment,
|
||||
required this.activeColor,
|
||||
required this.toggleable,
|
||||
required this.textStyle,
|
||||
required this.selectedTextStyle,
|
||||
required this.textPadding,
|
||||
this.optionWidth,
|
||||
this.verticalAlignment = WrapCrossAlignment.center,
|
||||
});
|
||||
|
||||
final T? groupValue;
|
||||
final List<T> items;
|
||||
final RadioButtonBuilder Function(T value) itemBuilder;
|
||||
final void Function(T?)? onChanged;
|
||||
final Axis direction;
|
||||
final double optionHeight;
|
||||
final double? optionWidth;
|
||||
final WrapAlignment horizontalAlignment;
|
||||
final WrapCrossAlignment verticalAlignment;
|
||||
final Color activeColor;
|
||||
final bool toggleable;
|
||||
final TextStyle textStyle;
|
||||
final TextStyle selectedTextStyle;
|
||||
final EdgeInsetsGeometry textPadding;
|
||||
|
||||
List<Widget> get _group => items.map(
|
||||
(item) {
|
||||
final radioButtonBuilder = itemBuilder(item);
|
||||
return SizedBox(
|
||||
height: optionHeight,
|
||||
width: optionWidth,
|
||||
child: RadioButton(
|
||||
description: radioButtonBuilder.description,
|
||||
value: item,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
buttonPosition: radioButtonBuilder.buttonPosition,
|
||||
activeColor: activeColor,
|
||||
toggleable: toggleable,
|
||||
textStyle: textStyle,
|
||||
selectedTextStyle: selectedTextStyle,
|
||||
textPadding: textPadding,
|
||||
shouldFlex: optionWidth != null,
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => direction == Axis.horizontal
|
||||
? Wrap(
|
||||
direction: direction,
|
||||
alignment: horizontalAlignment,
|
||||
children: _group,
|
||||
)
|
||||
: Wrap(
|
||||
direction: direction,
|
||||
crossAxisAlignment: verticalAlignment,
|
||||
children: _group,
|
||||
);
|
||||
}
|
||||
146
lib/src/widgets/flutter_flow_static_map.dart
Normal file
146
lib/src/widgets/flutter_flow_static_map.dart
Normal file
@@ -0,0 +1,146 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutterflow_ui/src/utils/lat_lng.dart';
|
||||
import 'package:mapbox_search/mapbox_search.dart' as mapbox;
|
||||
|
||||
/// A widget that displays a static map using the Mapbox API.
|
||||
class FlutterFlowStaticMap extends StatelessWidget {
|
||||
const FlutterFlowStaticMap({
|
||||
super.key,
|
||||
required this.location,
|
||||
required this.apiKey,
|
||||
required this.style,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.fit,
|
||||
this.borderRadius = BorderRadius.zero,
|
||||
this.markerColor,
|
||||
this.markerUrl,
|
||||
this.cached = false,
|
||||
this.zoom = 12,
|
||||
this.tilt = 0,
|
||||
this.rotation = 0,
|
||||
});
|
||||
|
||||
/// The location to display on the map.
|
||||
final LatLng location;
|
||||
|
||||
/// The API key for accessing the Mapbox API.
|
||||
final String apiKey;
|
||||
|
||||
/// The style of the map.
|
||||
final mapbox.MapBoxStyle style;
|
||||
|
||||
/// The width of the map widget.
|
||||
final double width;
|
||||
|
||||
/// The height of the map widget.
|
||||
final double height;
|
||||
|
||||
/// How the map should be inscribed into the available space.
|
||||
final BoxFit? fit;
|
||||
|
||||
/// The border radius of the map widget.
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
/// The color of the marker on the map.
|
||||
final Color? markerColor;
|
||||
|
||||
/// The URL of the custom marker icon.
|
||||
final String? markerUrl;
|
||||
|
||||
/// Whether to cache the map image.
|
||||
final bool cached;
|
||||
|
||||
/// The zoom level of the map.
|
||||
final int zoom;
|
||||
|
||||
/// The tilt angle of the map camera.
|
||||
final int tilt;
|
||||
|
||||
/// The rotation angle of the map camera.
|
||||
final int rotation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageWidth = width.clamp(1, 1280).toInt();
|
||||
final imageHeight = height.clamp(1, 1280).toInt();
|
||||
final imagePath = getStaticMapImageURL(location, apiKey, style, imageWidth,
|
||||
imageHeight, markerColor, markerUrl, zoom, rotation, tilt);
|
||||
return ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: cached
|
||||
? CachedNetworkImage(
|
||||
imageUrl: imagePath,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
)
|
||||
: Image.network(
|
||||
imagePath,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String getStaticMapImageURL(
|
||||
LatLng location,
|
||||
String apiKey,
|
||||
mapbox.MapBoxStyle mapStyle,
|
||||
int width,
|
||||
int height,
|
||||
Color? markerColor,
|
||||
String? markerURL,
|
||||
int zoom,
|
||||
int rotation,
|
||||
int tilt,
|
||||
) {
|
||||
final finalLocation = (
|
||||
lat: location.latitude.clamp(-90, 90).toDouble(),
|
||||
long: location.longitude.clamp(-180, 180).toDouble(),
|
||||
);
|
||||
final finalRotation = rotation.clamp(-180, 180).round();
|
||||
final finalTilt = tilt.clamp(0, 60).round();
|
||||
final finalZoom = zoom.clamp(0, 22).round();
|
||||
final image = mapbox.StaticImage(apiKey: apiKey);
|
||||
if (markerColor == null && (markerURL == null || markerURL.trim().isEmpty)) {
|
||||
return image
|
||||
.getStaticUrlWithoutMarker(
|
||||
center: finalLocation,
|
||||
style: mapStyle,
|
||||
width: width.round(),
|
||||
height: height.round(),
|
||||
zoomLevel: finalZoom,
|
||||
bearing: finalRotation,
|
||||
pitch: finalTilt,
|
||||
)
|
||||
.toString();
|
||||
} else {
|
||||
return image
|
||||
.getStaticUrlWithMarker(
|
||||
marker: markerURL == null || markerURL.trim().isEmpty
|
||||
? mapbox.MapBoxMarker(
|
||||
markerColor: mapbox.RgbColor(
|
||||
markerColor!.red,
|
||||
markerColor.green,
|
||||
markerColor.blue,
|
||||
),
|
||||
markerLetter: mapbox.MakiIcons.circle.value,
|
||||
markerSize: mapbox.MarkerSize.MEDIUM,
|
||||
)
|
||||
: null,
|
||||
markerUrl: markerURL,
|
||||
center: finalLocation,
|
||||
style: mapStyle,
|
||||
width: width.round(),
|
||||
height: height.round(),
|
||||
zoomLevel: finalZoom,
|
||||
bearing: finalRotation,
|
||||
pitch: finalTilt,
|
||||
)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
97
lib/src/widgets/flutter_flow_swipeable_stack.dart
Normal file
97
lib/src/widgets/flutter_flow_swipeable_stack.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
|
||||
|
||||
/// A widget that displays a stack of swipeable cards.
|
||||
class FlutterFlowSwipeableStack extends StatefulWidget {
|
||||
/// Creates a [FlutterFlowSwipeableStack].
|
||||
///
|
||||
/// - [itemBuilder] is a callback that builds the widget for each card in the stack.
|
||||
/// - [itemCount] is the total number of cards in the stack.
|
||||
/// - [controller] is the controller for the swipeable stack.
|
||||
/// - [onSwipeFn] is a callback that is called when a card is swiped.
|
||||
/// - [onRightSwipe] is a callback that is called when a card is swiped to the right.
|
||||
/// - [onLeftSwipe] is a callback that is called when a card is swiped to the left.
|
||||
/// - [onUpSwipe] is a callback that is called when a card is swiped up.
|
||||
/// - [onDownSwipe] is a callback that is called when a card is swiped down.
|
||||
/// - [loop] determines whether the stack should loop back to the beginning when the last card is swiped.
|
||||
/// - [cardDisplayCount] is the number of cards to display on the stack at a time.
|
||||
/// - [scale] is the scale factor for the cards in the stack.
|
||||
/// - [maxAngle] is the maximum rotation angle for the cards in the stack.
|
||||
/// - [threshold] is the swipe threshold for the cards in the stack.
|
||||
/// - [cardPadding] is the padding for each card in the stack.
|
||||
/// - [backCardOffset] is the offset for the back card in the stack.
|
||||
const FlutterFlowSwipeableStack({
|
||||
super.key,
|
||||
required this.itemBuilder,
|
||||
required this.itemCount,
|
||||
required this.controller,
|
||||
required this.onSwipeFn,
|
||||
required this.onRightSwipe,
|
||||
required this.onLeftSwipe,
|
||||
required this.onUpSwipe,
|
||||
required this.onDownSwipe,
|
||||
required this.loop,
|
||||
required this.cardDisplayCount,
|
||||
required this.scale,
|
||||
this.maxAngle,
|
||||
this.threshold,
|
||||
this.cardPadding,
|
||||
this.backCardOffset,
|
||||
});
|
||||
|
||||
final Widget Function(BuildContext, int) itemBuilder;
|
||||
final CardSwiperController controller;
|
||||
final int itemCount;
|
||||
final Function(int) onSwipeFn;
|
||||
final Function(int) onRightSwipe;
|
||||
final Function(int) onLeftSwipe;
|
||||
final Function(int) onUpSwipe;
|
||||
final Function(int) onDownSwipe;
|
||||
final bool loop;
|
||||
final int cardDisplayCount;
|
||||
final double scale;
|
||||
final double? maxAngle;
|
||||
final double? threshold;
|
||||
final EdgeInsetsGeometry? cardPadding;
|
||||
final Offset? backCardOffset;
|
||||
|
||||
@override
|
||||
State<FlutterFlowSwipeableStack> createState() => _FFSwipeableStackState();
|
||||
}
|
||||
|
||||
class _FFSwipeableStackState extends State<FlutterFlowSwipeableStack> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CardSwiper(
|
||||
controller: widget.controller,
|
||||
onSwipe: (previousIndex, currentIndex, direction) {
|
||||
widget.onSwipeFn(previousIndex);
|
||||
if (direction == CardSwiperDirection.left) {
|
||||
widget.onLeftSwipe(previousIndex);
|
||||
} else if (direction == CardSwiperDirection.right) {
|
||||
widget.onRightSwipe(previousIndex);
|
||||
} else if (direction == CardSwiperDirection.top) {
|
||||
widget.onUpSwipe(previousIndex);
|
||||
} else if (direction == CardSwiperDirection.bottom) {
|
||||
widget.onDownSwipe(previousIndex);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
cardsCount: widget.itemCount,
|
||||
cardBuilder: (context, index, percentThresholdX, percentThresholdY) {
|
||||
return widget.itemBuilder(context, index);
|
||||
},
|
||||
isLoop: widget.loop,
|
||||
maxAngle: widget.maxAngle ?? 30,
|
||||
threshold:
|
||||
widget.threshold != null ? (100 * widget.threshold!).round() : 50,
|
||||
scale: widget.scale,
|
||||
padding: widget.cardPadding ??
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 25),
|
||||
backCardOffset: widget.backCardOffset ?? const Offset(0, 40),
|
||||
numberOfCardsDisplayed: min(widget.cardDisplayCount, widget.itemCount),
|
||||
);
|
||||
}
|
||||
}
|
||||
144
lib/src/widgets/flutter_flow_timer.dart
Normal file
144
lib/src/widgets/flutter_flow_timer.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stop_watch_timer/stop_watch_timer.dart';
|
||||
|
||||
// Simple wrapper around StopWatchTimer that emits notifications on events.
|
||||
class FlutterFlowTimerController with ChangeNotifier {
|
||||
FlutterFlowTimerController(this.timer);
|
||||
final StopWatchTimer timer;
|
||||
|
||||
void onStartTimer() {
|
||||
timer.onStartTimer();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void onStopTimer() {
|
||||
timer.onStopTimer();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void onResetTimer() {
|
||||
timer.onResetTimer();
|
||||
late final StreamSubscription subscription;
|
||||
// We can't notify listeners right away: they'll see the old timer value.
|
||||
// We need to wait until the next time is emitted.
|
||||
subscription = timer.rawTime.listen((_) {
|
||||
notifyListeners();
|
||||
subscription.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// A timer widget that displays and manages time.
|
||||
class FlutterFlowTimer extends StatefulWidget {
|
||||
/// Creates a [FlutterFlowTimer] widget.
|
||||
const FlutterFlowTimer({
|
||||
super.key,
|
||||
required this.initialTime,
|
||||
required this.controller,
|
||||
required this.getDisplayTime,
|
||||
required this.onChanged,
|
||||
this.updateStateInterval,
|
||||
this.onEnded,
|
||||
required this.textAlign,
|
||||
required this.style,
|
||||
});
|
||||
|
||||
/// The initial time for the timer.
|
||||
final int initialTime;
|
||||
|
||||
/// The controller for the timer.
|
||||
final FlutterFlowTimerController controller;
|
||||
|
||||
/// A function that returns the formatted display time.
|
||||
final String Function(int) getDisplayTime;
|
||||
|
||||
/// A callback function that is called when the timer value changes.
|
||||
final Function(int value, String displayTime, bool shouldUpdate) onChanged;
|
||||
|
||||
/// The interval at which the timer state should be updated.
|
||||
final Duration? updateStateInterval;
|
||||
|
||||
/// A callback function that is called when the timer ends.
|
||||
final Function()? onEnded;
|
||||
|
||||
/// The alignment of the timer text.
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// The style of the timer text.
|
||||
final TextStyle style;
|
||||
|
||||
@override
|
||||
State<FlutterFlowTimer> createState() => _FlutterFlowTimerState();
|
||||
}
|
||||
|
||||
class _FlutterFlowTimerState extends State<FlutterFlowTimer> {
|
||||
int get timerValue => widget.controller.timer.rawTime.value;
|
||||
bool get isCountUp => widget.controller.timer.mode == StopWatchMode.countUp;
|
||||
|
||||
late String _displayTime;
|
||||
late int lastUpdateMs;
|
||||
|
||||
Function() get onEnded => widget.onEnded ?? () {};
|
||||
|
||||
void _initTimer({required bool shouldUpdate}) {
|
||||
// Initialize timer display time and last update time.
|
||||
_displayTime = widget.getDisplayTime(widget.controller.timer.rawTime.value);
|
||||
lastUpdateMs = timerValue;
|
||||
// Update timer value and display time.
|
||||
widget.onChanged(timerValue, _displayTime, shouldUpdate);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Set the initial time.
|
||||
widget.controller.timer.setPresetTime(mSec: widget.initialTime, add: false);
|
||||
// Initialize timer properties without updating outer state.
|
||||
_initTimer(shouldUpdate: false);
|
||||
// Add a listener for when the timer value changes to update the
|
||||
// displayed timer value.
|
||||
widget.controller.timer.rawTime.listen((_) {
|
||||
_displayTime = widget.getDisplayTime(timerValue);
|
||||
widget.onChanged(timerValue, _displayTime, _shouldUpdate());
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
// Add listener for actions executed on timer.
|
||||
widget.controller.addListener(() => _initTimer(shouldUpdate: true));
|
||||
|
||||
// Add listener for when the timer ends.
|
||||
widget.controller.timer.fetchEnded.listen((_) => onEnded());
|
||||
}
|
||||
|
||||
bool _shouldUpdate() {
|
||||
// If a null or 0ms update interval is provided, always update.
|
||||
final updateIntervalMs = widget.updateStateInterval?.inMilliseconds;
|
||||
if (updateIntervalMs == null || updateIntervalMs == 0) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise, we only update after the specified duration has passed
|
||||
// since the most recent update.
|
||||
final cutoff = lastUpdateMs + updateIntervalMs * (isCountUp ? 1 : -1);
|
||||
final shouldUpdate = isCountUp ? timerValue > cutoff : timerValue < cutoff;
|
||||
if (shouldUpdate) {
|
||||
lastUpdateMs = timerValue;
|
||||
}
|
||||
return shouldUpdate;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Text(
|
||||
_displayTime,
|
||||
textAlign: widget.textAlign,
|
||||
style: widget.style,
|
||||
);
|
||||
}
|
||||
36
lib/src/widgets/flutter_flow_toggle_icon.dart
Normal file
36
lib/src/widgets/flutter_flow_toggle_icon.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that represents a toggle icon.
|
||||
class ToggleIcon extends StatelessWidget {
|
||||
/// Creates a [ToggleIcon].
|
||||
///
|
||||
/// - [value] parameter specifies whether the icon is currently toggled on or off.
|
||||
/// - [onPressed] parameter is a callback function that is called when the icon is pressed.
|
||||
/// - [onIcon] parameter specifies the widget to display when the icon is toggled on.
|
||||
/// - [offIcon] parameter specifies the widget to display when the icon is toggled off.
|
||||
const ToggleIcon({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onPressed,
|
||||
required this.onIcon,
|
||||
required this.offIcon,
|
||||
});
|
||||
|
||||
/// Whether the icon is currently toggled on or off.
|
||||
final bool value;
|
||||
|
||||
/// A callback function that is called when the icon is pressed.
|
||||
final Function() onPressed;
|
||||
|
||||
/// The widget to display when the icon is toggled on.
|
||||
final Widget onIcon;
|
||||
|
||||
/// The widget to display when the icon is toggled off.
|
||||
final Widget offIcon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: value ? onIcon : offIcon,
|
||||
);
|
||||
}
|
||||
140
lib/src/widgets/flutter_flow_web_view.dart
Normal file
140
lib/src/widgets/flutter_flow_web_view.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutterflow_ui/src/utils/flutter_flow_helpers.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart' hide NavigationDecision;
|
||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||
import 'package:webviewx_plus/webviewx_plus.dart';
|
||||
|
||||
/// A widget that displays web content in a WebView.
|
||||
class FlutterFlowWebView extends StatefulWidget {
|
||||
/// Creates a [FlutterFlowWebView] widget.
|
||||
///
|
||||
/// - [content] parameter specifies the web content to be displayed.
|
||||
/// - [width] and [height] parameters specify the dimensions of the WebView.
|
||||
/// - [bypass] parameter determines whether to bypass the WebView and open the content in the default browser.
|
||||
/// - [horizontalScroll] parameter determines whether to enable horizontal scrolling in the WebView.
|
||||
/// - [verticalScroll] parameter determines whether to enable vertical scrolling in the WebView.
|
||||
/// - [html] parameter determines whether the content is HTML.
|
||||
const FlutterFlowWebView({
|
||||
super.key,
|
||||
required this.content,
|
||||
this.width,
|
||||
this.height,
|
||||
this.bypass = false,
|
||||
this.horizontalScroll = false,
|
||||
this.verticalScroll = false,
|
||||
this.html = false,
|
||||
});
|
||||
|
||||
/// The web content to be displayed in the WebView.
|
||||
final String content;
|
||||
|
||||
/// The width of the WebView.
|
||||
final double? width;
|
||||
|
||||
/// The height of the WebView.
|
||||
final double? height;
|
||||
|
||||
/// Determines whether to bypass the WebView and open the content in the default browser.
|
||||
final bool bypass;
|
||||
|
||||
/// Determines whether to enable horizontal scrolling in the WebView.
|
||||
final bool horizontalScroll;
|
||||
|
||||
/// Determines whether to enable vertical scrolling in the WebView.
|
||||
final bool verticalScroll;
|
||||
|
||||
/// Determines whether the content is HTML.
|
||||
final bool html;
|
||||
|
||||
@override
|
||||
State<FlutterFlowWebView> createState() => _FlutterFlowWebViewState();
|
||||
}
|
||||
|
||||
class _FlutterFlowWebViewState extends State<FlutterFlowWebView> {
|
||||
@override
|
||||
Widget build(BuildContext context) => WebViewX(
|
||||
key: webviewKey,
|
||||
width: widget.width ?? MediaQuery.sizeOf(context).width,
|
||||
height: widget.height ?? MediaQuery.sizeOf(context).height,
|
||||
ignoreAllGestures: false,
|
||||
initialContent: widget.content,
|
||||
initialMediaPlaybackPolicy:
|
||||
AutoMediaPlaybackPolicy.requireUserActionForAllMediaTypes,
|
||||
initialSourceType: widget.html
|
||||
? SourceType.html
|
||||
: widget.bypass
|
||||
? SourceType.urlBypass
|
||||
: SourceType.url,
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
onWebViewCreated: (controller) async {
|
||||
if (controller.connector is WebViewController && isAndroid) {
|
||||
final androidController =
|
||||
controller.connector.platform as AndroidWebViewController;
|
||||
await androidController.setOnShowFileSelector(_androidFilePicker);
|
||||
}
|
||||
},
|
||||
navigationDelegate: (request) async {
|
||||
if (isAndroid) {
|
||||
if (request.content.source
|
||||
.startsWith('https://api.whatsapp.com/send?phone')) {
|
||||
String url = request.content.source;
|
||||
|
||||
await launchUrl(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
webSpecificParams: const WebSpecificParams(
|
||||
webAllowFullscreenContent: true,
|
||||
),
|
||||
mobileSpecificParams: MobileSpecificParams(
|
||||
debuggingEnabled: false,
|
||||
gestureNavigationEnabled: true,
|
||||
mobileGestureRecognizers: {
|
||||
if (widget.verticalScroll)
|
||||
const Factory<VerticalDragGestureRecognizer>(
|
||||
VerticalDragGestureRecognizer.new,
|
||||
),
|
||||
if (widget.horizontalScroll)
|
||||
const Factory<HorizontalDragGestureRecognizer>(
|
||||
HorizontalDragGestureRecognizer.new,
|
||||
),
|
||||
},
|
||||
androidEnableHybridComposition: true,
|
||||
),
|
||||
);
|
||||
|
||||
Key get webviewKey => Key(
|
||||
[
|
||||
widget.content,
|
||||
widget.width,
|
||||
widget.height,
|
||||
widget.bypass,
|
||||
widget.horizontalScroll,
|
||||
widget.verticalScroll,
|
||||
widget.html,
|
||||
].map((s) => s?.toString() ?? '').join(),
|
||||
);
|
||||
|
||||
Future<List<String>> _androidFilePicker(
|
||||
final FileSelectorParams params,
|
||||
) async {
|
||||
final result = await FilePicker.platform.pickFiles();
|
||||
|
||||
if (result != null && result.files.single.path != null) {
|
||||
final file = File(result.files.single.path!);
|
||||
return [file.uri.toString()];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
25
lib/src/widgets/flutter_flow_widgets.dart
Normal file
25
lib/src/widgets/flutter_flow_widgets.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
// export 'flutter_flow_ad_banner.dart';
|
||||
export 'flutter_flow_autocomplete_options_list.dart';
|
||||
export 'flutter_flow_button.dart';
|
||||
export 'flutter_flow_button_tabbar.dart';
|
||||
export 'flutter_flow_calendar.dart';
|
||||
export 'flutter_flow_charts.dart';
|
||||
export 'flutter_flow_checkbox_group.dart';
|
||||
export 'flutter_flow_choice_chips.dart';
|
||||
export 'flutter_flow_count_controller.dart';
|
||||
export 'flutter_flow_credit_card_form.dart';
|
||||
export 'flutter_flow_data_table.dart';
|
||||
export 'flutter_flow_drop_down.dart';
|
||||
export 'flutter_flow_expanded_image_view.dart';
|
||||
// export 'flutter_flow_google_map.dart';
|
||||
export 'flutter_flow_icon_button.dart';
|
||||
export 'flutter_flow_language_selector.dart';
|
||||
export 'flutter_flow_media_display.dart';
|
||||
export 'flutter_flow_mux_broadcast.dart';
|
||||
export 'flutter_flow_radio_button.dart';
|
||||
export 'flutter_flow_static_map.dart';
|
||||
export 'flutter_flow_swipeable_stack.dart';
|
||||
export 'flutter_flow_timer.dart';
|
||||
export 'flutter_flow_toggle_icon.dart';
|
||||
export 'flutter_flow_web_view.dart';
|
||||
export 'keep_alive_wrapper.dart';
|
||||
25
lib/src/widgets/keep_alive_wrapper.dart
Normal file
25
lib/src/widgets/keep_alive_wrapper.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class KeepAliveWidgetWrapper extends StatefulWidget {
|
||||
const KeepAliveWidgetWrapper({
|
||||
super.key,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final WidgetBuilder builder;
|
||||
|
||||
@override
|
||||
State<KeepAliveWidgetWrapper> createState() => _KeepAliveWidgetWrapperState();
|
||||
}
|
||||
|
||||
class _KeepAliveWidgetWrapperState extends State<KeepAliveWidgetWrapper>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return widget.builder(context);
|
||||
}
|
||||
}
|
||||
83
pubspec.yaml
Normal file
83
pubspec.yaml
Normal 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
|
||||
Reference in New Issue
Block a user