From 0838b7b0171db4dd81d0ccf29378a33380bb96ef Mon Sep 17 00:00:00 2001 From: Freek van de Ven <35540954+freekvandeven@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:12:48 +0200 Subject: [PATCH] Feature/melos-variant (#8) * feat: add flutter_shopping package * feat: add flutter_shopping_cart package * feat: add flutter_order_details package * feat: add flutter_product_page package * feat: remove example apps * feat: update pubspecs * feat: add github actions * feat: export all the components through the flutter_shopping --- .github/dependabot.yml | 22 ++ .github/workflows/component-documentation.yml | 14 + .github/workflows/melos-ci.yml | 14 + .gitignore | 43 +-- CHANGELOG.md | 3 + LICENSE | 9 + lib/flutter_shopping.dart | 7 - linux/.gitignore | 1 - linux/CMakeLists.txt | 145 ------- linux/flutter/CMakeLists.txt | 88 ----- linux/flutter/generated_plugin_registrant.cc | 11 - linux/flutter/generated_plugin_registrant.h | 15 - linux/flutter/generated_plugins.cmake | 23 -- linux/main.cc | 6 - linux/my_application.cc | 124 ------ linux/my_application.h | 18 - melos.yaml | 39 ++ packages/flutter_order_details/.gitignore | 49 +++ packages/flutter_order_details/README.md | 36 ++ .../analysis_options.yaml | 0 .../lib/flutter_order_details.dart | 17 + .../order_detail_configuration.dart | 50 +++ .../order_detail_localization.dart | 18 + .../src/configuration/order_detail_step.dart | 22 ++ .../order_detail_title_style.dart | 14 + .../src/models/formfield_error_builder.dart | 25 ++ .../lib/src/models/order_address_input.dart | 160 ++++++++ .../lib/src/models/order_choice_input.dart | 175 +++++++++ .../lib/src/models/order_dropdown_input.dart | 153 ++++++++ .../lib/src/models/order_email_input.dart | 75 ++++ .../lib/src/models/order_input.dart | 173 +++++++++ .../lib/src/models/order_phone_input.dart | 101 +++++ .../lib/src/models/order_result.dart | 14 + .../lib/src/models/order_text_input.dart | 71 ++++ .../src/models/order_time_picker_input.dart | 353 ++++++++++++++++++ .../lib/src/widgets/order_detail_screen.dart | 273 ++++++++++++++ packages/flutter_order_details/pubspec.yaml | 30 ++ packages/flutter_product_page/.gitignore | 56 +++ packages/flutter_product_page/README.md | 47 +++ .../analysis_options.yaml | 9 + .../lib/flutter_product_page.dart | 13 + ...t_page_category_styling_configuration.dart | 54 +++ .../product_page_configuration.dart | 202 ++++++++++ .../configuration/product_page_content.dart | 16 + .../product_page_localization.dart | 22 ++ .../product_page_shop_selector_style.dart | 8 + .../lib/src/models/product.dart | 26 ++ .../lib/src/models/product_page_shop.dart | 18 + .../lib/src/services/category_service.dart | 73 ++++ .../src/services/selected_shop_service.dart | 21 ++ .../src/services/shopping_cart_notifier.dart | 10 + .../lib/src/ui/components/product_item.dart | 195 ++++++++++ .../lib/src/ui/components/shop_selector.dart | 63 ++++ .../src/ui/components/weekly_discount.dart | 127 +++++++ .../lib/src/ui/product_page.dart | 288 ++++++++++++++ .../lib/src/ui/product_page_screen.dart | 36 ++ .../src/ui/widgets/horizontal_list_items.dart | 73 ++++ .../src/ui/widgets/product_item_popup.dart | 69 ++++ .../lib/src/ui/widgets/spaced_wrap.dart | 150 ++++++++ packages/flutter_product_page/pubspec.yaml | 38 ++ packages/flutter_shopping/.gitignore | 56 +++ .../flutter_shopping/README.md | 0 .../flutter_shopping/analysis_options.yaml | 9 + .../flutter_shopping/example}/.gitignore | 0 .../flutter_shopping/example}/README.md | 0 .../example}/analysis_options.yaml | 0 .../flutter_shopping/example}/lib/main.dart | 0 .../lib/src/configuration/configuration.dart | 3 - .../example}/lib/src/models/my_product.dart | 3 +- .../example}/lib/src/models/my_shop.dart | 2 +- .../example}/lib/src/routes.dart | 0 .../lib/src/services/order_service.dart | 2 +- .../lib/src/services/shop_service.dart | 2 +- .../example}/lib/src/ui/homepage.dart | 0 .../example}/lib/src/utils/go_router.dart | 0 .../example}/lib/src/utils/theme.dart | 0 .../flutter_shopping/example}/pubspec.yaml | 16 +- .../example_amazon}/.gitignore | 0 .../example_amazon}/README.md | 0 .../example_amazon}/analysis_options.yaml | 0 .../example_amazon}/lib/main.dart | 0 .../configuration/shopping_configuration.dart | 2 - .../lib/src/models/my_category.dart | 2 +- .../lib/src/models/my_product.dart | 3 +- .../example_amazon}/lib/src/routes.dart | 0 .../lib/src/services/category_service.dart | 2 +- .../example_amazon}/lib/src/ui/homepage.dart | 0 .../lib/src/utils/go_router.dart | 0 .../example_amazon}/lib/src/utils/theme.dart | 0 .../example_amazon}/pubspec.yaml | 18 +- .../example_amazon}/test/widget_test.dart | 0 .../lib/flutter_shopping.dart | 11 + .../default_order_detail_configuration.dart | 1 - .../flutter_shopping_configuration.dart | 0 .../flutter_shopping/lib}/src/go_router.dart | 0 .../flutter_shopping/lib}/src/routes.dart | 0 .../flutter_shopping_userstory_go_router.dart | 1 - ...flutter_shopping_userstory_navigation.dart | 1 - .../widgets/default_order_failed_widget.dart | 0 .../widgets/default_order_succes_widget.dart | 0 packages/flutter_shopping/pubspec.yaml | 47 +++ packages/flutter_shopping_cart/.gitignore | 49 +++ packages/flutter_shopping_cart/README.md | 77 ++++ .../analysis_options.yaml | 9 + .../lib/flutter_shopping_cart.dart | 8 + .../lib/src/config/shopping_cart_config.dart | 133 +++++++ .../config/shopping_cart_localizations.dart | 24 ++ .../lib/src/models/shopping_cart_product.dart | 29 ++ .../lib/src/services/product_service.dart | 71 ++++ .../lib/src/widgets/shopping_cart_screen.dart | 232 ++++++++++++ packages/flutter_shopping_cart/pubspec.yaml | 21 ++ pubspec.yaml | 44 +-- 112 files changed, 4330 insertions(+), 553 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/component-documentation.yml create mode 100644 .github/workflows/melos-ci.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE delete mode 100644 lib/flutter_shopping.dart delete mode 100644 linux/.gitignore delete mode 100644 linux/CMakeLists.txt delete mode 100644 linux/flutter/CMakeLists.txt delete mode 100644 linux/flutter/generated_plugin_registrant.cc delete mode 100644 linux/flutter/generated_plugin_registrant.h delete mode 100644 linux/flutter/generated_plugins.cmake delete mode 100644 linux/main.cc delete mode 100644 linux/my_application.cc delete mode 100644 linux/my_application.h create mode 100644 melos.yaml create mode 100644 packages/flutter_order_details/.gitignore create mode 100644 packages/flutter_order_details/README.md rename analysis_options.yaml => packages/flutter_order_details/analysis_options.yaml (100%) create mode 100644 packages/flutter_order_details/lib/flutter_order_details.dart create mode 100644 packages/flutter_order_details/lib/src/configuration/order_detail_configuration.dart create mode 100644 packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart create mode 100644 packages/flutter_order_details/lib/src/configuration/order_detail_step.dart create mode 100644 packages/flutter_order_details/lib/src/configuration/order_detail_title_style.dart create mode 100644 packages/flutter_order_details/lib/src/models/formfield_error_builder.dart create mode 100644 packages/flutter_order_details/lib/src/models/order_address_input.dart create mode 100644 packages/flutter_order_details/lib/src/models/order_choice_input.dart create mode 100644 packages/flutter_order_details/lib/src/models/order_dropdown_input.dart create mode 100644 packages/flutter_order_details/lib/src/models/order_email_input.dart create mode 100644 packages/flutter_order_details/lib/src/models/order_input.dart create mode 100644 packages/flutter_order_details/lib/src/models/order_phone_input.dart create mode 100644 packages/flutter_order_details/lib/src/models/order_result.dart create mode 100644 packages/flutter_order_details/lib/src/models/order_text_input.dart create mode 100644 packages/flutter_order_details/lib/src/models/order_time_picker_input.dart create mode 100644 packages/flutter_order_details/lib/src/widgets/order_detail_screen.dart create mode 100644 packages/flutter_order_details/pubspec.yaml create mode 100644 packages/flutter_product_page/.gitignore create mode 100644 packages/flutter_product_page/README.md create mode 100644 packages/flutter_product_page/analysis_options.yaml create mode 100644 packages/flutter_product_page/lib/flutter_product_page.dart create mode 100644 packages/flutter_product_page/lib/src/configuration/product_page_category_styling_configuration.dart create mode 100644 packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart create mode 100644 packages/flutter_product_page/lib/src/configuration/product_page_content.dart create mode 100644 packages/flutter_product_page/lib/src/configuration/product_page_localization.dart create mode 100644 packages/flutter_product_page/lib/src/configuration/product_page_shop_selector_style.dart create mode 100644 packages/flutter_product_page/lib/src/models/product.dart create mode 100644 packages/flutter_product_page/lib/src/models/product_page_shop.dart create mode 100644 packages/flutter_product_page/lib/src/services/category_service.dart create mode 100644 packages/flutter_product_page/lib/src/services/selected_shop_service.dart create mode 100644 packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart create mode 100644 packages/flutter_product_page/lib/src/ui/components/product_item.dart create mode 100644 packages/flutter_product_page/lib/src/ui/components/shop_selector.dart create mode 100644 packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart create mode 100644 packages/flutter_product_page/lib/src/ui/product_page.dart create mode 100644 packages/flutter_product_page/lib/src/ui/product_page_screen.dart create mode 100644 packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart create mode 100644 packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart create mode 100644 packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart create mode 100644 packages/flutter_product_page/pubspec.yaml create mode 100644 packages/flutter_shopping/.gitignore rename README.md => packages/flutter_shopping/README.md (100%) create mode 100644 packages/flutter_shopping/analysis_options.yaml rename {example => packages/flutter_shopping/example}/.gitignore (100%) rename {example => packages/flutter_shopping/example}/README.md (100%) rename {example => packages/flutter_shopping/example}/analysis_options.yaml (100%) rename {example => packages/flutter_shopping/example}/lib/main.dart (100%) rename {example => packages/flutter_shopping/example}/lib/src/configuration/configuration.dart (97%) rename {example => packages/flutter_shopping/example}/lib/src/models/my_product.dart (76%) rename {example => packages/flutter_shopping/example}/lib/src/models/my_shop.dart (63%) rename {example => packages/flutter_shopping/example}/lib/src/routes.dart (100%) rename {example => packages/flutter_shopping/example}/lib/src/services/order_service.dart (75%) rename {example => packages/flutter_shopping/example}/lib/src/services/shop_service.dart (95%) rename {example => packages/flutter_shopping/example}/lib/src/ui/homepage.dart (100%) rename {example => packages/flutter_shopping/example}/lib/src/utils/go_router.dart (100%) rename {example => packages/flutter_shopping/example}/lib/src/utils/theme.dart (100%) rename {example => packages/flutter_shopping/example}/pubspec.yaml (67%) rename {example_amazon => packages/flutter_shopping/example_amazon}/.gitignore (100%) rename {example_amazon => packages/flutter_shopping/example_amazon}/README.md (100%) rename {example_amazon => packages/flutter_shopping/example_amazon}/analysis_options.yaml (100%) rename {example_amazon => packages/flutter_shopping/example_amazon}/lib/main.dart (100%) rename {example_amazon => packages/flutter_shopping/example_amazon}/lib/src/configuration/shopping_configuration.dart (99%) rename {example_amazon => packages/flutter_shopping/example_amazon}/lib/src/models/my_category.dart (65%) rename {example_amazon => packages/flutter_shopping/example_amazon}/lib/src/models/my_product.dart (75%) rename {example_amazon => packages/flutter_shopping/example_amazon}/lib/src/routes.dart (100%) rename {example_amazon => packages/flutter_shopping/example_amazon}/lib/src/services/category_service.dart (97%) rename {example_amazon => packages/flutter_shopping/example_amazon}/lib/src/ui/homepage.dart (100%) rename {example_amazon => packages/flutter_shopping/example_amazon}/lib/src/utils/go_router.dart (100%) rename {example_amazon => packages/flutter_shopping/example_amazon}/lib/src/utils/theme.dart (100%) rename {example_amazon => packages/flutter_shopping/example_amazon}/pubspec.yaml (50%) rename {example_amazon => packages/flutter_shopping/example_amazon}/test/widget_test.dart (100%) create mode 100644 packages/flutter_shopping/lib/flutter_shopping.dart rename {lib => packages/flutter_shopping/lib}/src/config/default_order_detail_configuration.dart (97%) rename {lib => packages/flutter_shopping/lib}/src/config/flutter_shopping_configuration.dart (100%) rename {lib => packages/flutter_shopping/lib}/src/go_router.dart (100%) rename {lib => packages/flutter_shopping/lib}/src/routes.dart (100%) rename {lib => packages/flutter_shopping/lib}/src/user_stores/flutter_shopping_userstory_go_router.dart (96%) rename {lib => packages/flutter_shopping/lib}/src/user_stores/flutter_shopping_userstory_navigation.dart (95%) rename {lib => packages/flutter_shopping/lib}/src/widgets/default_order_failed_widget.dart (100%) rename {lib => packages/flutter_shopping/lib}/src/widgets/default_order_succes_widget.dart (100%) create mode 100644 packages/flutter_shopping/pubspec.yaml create mode 100644 packages/flutter_shopping_cart/.gitignore create mode 100644 packages/flutter_shopping_cart/README.md create mode 100644 packages/flutter_shopping_cart/analysis_options.yaml create mode 100644 packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart create mode 100644 packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart create mode 100644 packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart create mode 100644 packages/flutter_shopping_cart/lib/src/models/shopping_cart_product.dart create mode 100644 packages/flutter_shopping_cart/lib/src/services/product_service.dart create mode 100644 packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart create mode 100644 packages/flutter_shopping_cart/pubspec.yaml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8a10716 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 + +updates: + - package-ecosystem: "pub" + directory: "/packages/flutter_order_details" + schedule: + interval: "weekly" + + - package-ecosystem: "pub" + directory: "/packages/flutter_product_page" + schedule: + interval: "weekly" + + - package-ecosystem: "pub" + directory: "/packages/flutter_shopping" + schedule: + interval: "weekly" + + - package-ecosystem: "pub" + directory: "/packages/flutter_shopping_cart" + schedule: + interval: "weekly" diff --git a/.github/workflows/component-documentation.yml b/.github/workflows/component-documentation.yml new file mode 100644 index 0000000..c12e46a --- /dev/null +++ b/.github/workflows/component-documentation.yml @@ -0,0 +1,14 @@ +name: Iconica Standard Component Documentation Workflow +# Workflow Caller version: 1.0.0 + +on: + release: + types: [published] + + workflow_dispatch: + +jobs: + call-iconica-component-documentation-workflow: + uses: Iconica-Development/.github/.github/workflows/component-documentation.yml@master + secrets: inherit + permissions: write-all diff --git a/.github/workflows/melos-ci.yml b/.github/workflows/melos-ci.yml new file mode 100644 index 0000000..a7ad1d2 --- /dev/null +++ b/.github/workflows/melos-ci.yml @@ -0,0 +1,14 @@ +name: Iconica Standard Melos CI Workflow +# Workflow Caller version: 1.0.0 + +on: + pull_request: + workflow_dispatch: + +jobs: + call-global-iconica-workflow: + uses: Iconica-Development/.github/.github/workflows/melos-ci.yml@master + secrets: inherit + permissions: write-all + with: + subfolder: '.' # add optional subfolder to run workflow in diff --git a/.gitignore b/.gitignore index 95090c7..467e31c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2024 Iconica +# +# SPDX-License-Identifier: GPL-3.0-or-later + # Miscellaneous *.class *.log @@ -9,13 +13,13 @@ .history .svn/ migrate_working_dir/ -.metadata # IntelliJ related *.iml *.ipr *.iws .idea/ +ios # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line @@ -23,33 +27,20 @@ migrate_working_dir/ .vscode/ # Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +# /pubspec.lock **/doc/api/ -**/ios/Flutter/.last_build_id .dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies .packages -.pub-cache/ -.pub/ -/build/ +build/ +.flutter-plugins-dependencies +.flutter-plugins +.metadata + pubspec.lock +packages/flutter_chat/pubspec.lock +packages/flutter_chat_firebase/pubspec.lock +packages/flutter_chat_interface/pubspec.lock +packages/flutter_chat_view/pubspec.lock -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release - -# env -*dotenv - -android/ -ios/ -web/ -macos/ -windows/ \ No newline at end of file +pubspec_overrides.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7bc72cc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version of the combined melos variant of the flutter_shopping user-story. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a0dd757 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2024 Iconica, All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + 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. + 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. diff --git a/lib/flutter_shopping.dart b/lib/flutter_shopping.dart deleted file mode 100644 index 3cdd31e..0000000 --- a/lib/flutter_shopping.dart +++ /dev/null @@ -1,7 +0,0 @@ -/// Flutter Shopping -library flutter_shopping; - -export "package:flutter_shopping/src/config/flutter_shopping_configuration.dart"; -export "package:flutter_shopping/src/routes.dart"; -export "package:flutter_shopping/src/user_stores/flutter_shopping_userstory_go_router.dart"; -export "package:flutter_shopping/src/user_stores/flutter_shopping_userstory_navigation.dart"; diff --git a/linux/.gitignore b/linux/.gitignore deleted file mode 100644 index d3896c9..0000000 --- a/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt deleted file mode 100644 index 03fc5bc..0000000 --- a/linux/CMakeLists.txt +++ /dev/null @@ -1,145 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.10) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "flutter_shopping") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.iconica.flutter_shopping") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Define the application target. To change its name, change BINARY_NAME above, -# not the value here, or `flutter run` will no longer work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd016..0000000 --- a/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d..0000000 --- a/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47..0000000 --- a/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake deleted file mode 100644 index 2e1de87..0000000 --- a/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc deleted file mode 100644 index e7c5c54..0000000 --- a/linux/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/linux/my_application.cc b/linux/my_application.cc deleted file mode 100644 index 7a9a401..0000000 --- a/linux/my_application.cc +++ /dev/null @@ -1,124 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "flutter_shopping"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "flutter_shopping"); - } - - gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application startup. - - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); -} - -// Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application shutdown. - - G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; - G_APPLICATION_CLASS(klass)->startup = my_application_startup; - G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, - nullptr)); -} diff --git a/linux/my_application.h b/linux/my_application.h deleted file mode 100644 index 72271d5..0000000 --- a/linux/my_application.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/melos.yaml b/melos.yaml new file mode 100644 index 0000000..8c768c1 --- /dev/null +++ b/melos.yaml @@ -0,0 +1,39 @@ +name: flutter_shopping + +packages: + - packages/** + +command: + version: + branch: master + +scripts: + lint:all: + run: dart run melos run analyze && dart run melos run format-check + description: Run all static analysis checks. + + get: + run: | + melos exec -c 1 -- "flutter pub get" + melos exec --scope="*example*" -c 1 -- "flutter pub get" + + upgrade: + run: melos exec -c 1 -- "flutter pub upgrade" + + create: + # run create in the example folder of flutter_chat_view + run: melos exec --scope="*example*" -c 1 -- "flutter create ." + + analyze: + run: | + dart run melos exec -c 1 -- \ + flutter analyze --fatal-infos + description: Run `flutter analyze` for all packages. + + format: + run: dart run melos exec dart format . + description: Run `dart format` for all packages. + + format-check: + run: dart run melos exec dart format . --set-exit-if-changed + description: Run `dart format` checks for all packages. diff --git a/packages/flutter_order_details/.gitignore b/packages/flutter_order_details/.gitignore new file mode 100644 index 0000000..e31020f --- /dev/null +++ b/packages/flutter_order_details/.gitignore @@ -0,0 +1,49 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ +.metadata + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# env +*dotenv diff --git a/packages/flutter_order_details/README.md b/packages/flutter_order_details/README.md new file mode 100644 index 0000000..b736730 --- /dev/null +++ b/packages/flutter_order_details/README.md @@ -0,0 +1,36 @@ +# flutter_order_details + +This component contains TODO... + +## Features + +* TODO... + +## Usage + +First, TODO... + +For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_order_details/tree/main/example). + +Or, you could run the example yourself: +``` +git clone https://github.com/Iconica-Development/flutter_order_details.git + +cd flutter_order_details + +cd example + +flutter run +``` + +## Issues + +Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_order_details) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). + +## Want to contribute + +If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_order_details/pulls). + +## Author + +This flutter_order_details for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at diff --git a/analysis_options.yaml b/packages/flutter_order_details/analysis_options.yaml similarity index 100% rename from analysis_options.yaml rename to packages/flutter_order_details/analysis_options.yaml diff --git a/packages/flutter_order_details/lib/flutter_order_details.dart b/packages/flutter_order_details/lib/flutter_order_details.dart new file mode 100644 index 0000000..0694d28 --- /dev/null +++ b/packages/flutter_order_details/lib/flutter_order_details.dart @@ -0,0 +1,17 @@ +/// Flutter component for shopping cart. +library flutter_order_details; + +export "src/configuration/order_detail_configuration.dart"; +export "src/configuration/order_detail_localization.dart"; +export "src/configuration/order_detail_step.dart"; +export "src/configuration/order_detail_title_style.dart"; +export "src/models/order_address_input.dart"; +export "src/models/order_choice_input.dart"; +export "src/models/order_dropdown_input.dart"; +export "src/models/order_email_input.dart"; +export "src/models/order_input.dart"; +export "src/models/order_phone_input.dart"; +export "src/models/order_result.dart"; +export "src/models/order_text_input.dart"; +export "src/models/order_time_picker_input.dart"; +export "src/widgets/order_detail_screen.dart"; diff --git a/packages/flutter_order_details/lib/src/configuration/order_detail_configuration.dart b/packages/flutter_order_details/lib/src/configuration/order_detail_configuration.dart new file mode 100644 index 0000000..1aaa75f --- /dev/null +++ b/packages/flutter_order_details/lib/src/configuration/order_detail_configuration.dart @@ -0,0 +1,50 @@ +import "package:flutter/widgets.dart"; +import "package:flutter_order_details/src/configuration/order_detail_localization.dart"; +import "package:flutter_order_details/src/configuration/order_detail_step.dart"; +import "package:flutter_order_details/src/models/order_result.dart"; + +/// Configuration for the order detail screen. +class OrderDetailConfiguration { + /// Constructor for the order detail configuration. + const OrderDetailConfiguration({ + required this.steps, + // + required this.onCompleted, + // + this.progressIndicator = true, + // + this.localization = const OrderDetailLocalization(), + // + this.inputFieldPadding = const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + this.titlePadding = const EdgeInsets.only(left: 16, right: 16, top: 16), + // + this.appBar, + }); + + /// The different steps that the user has to go through to complete the order. + /// Each step contains a list of fields that the user has to fill in. + final List steps; + + /// Callback function that is called when the user has completed the order. + /// The result of the order is passed as an argument to the function. + final Function(OrderResult result) onCompleted; + + /// Whether or not you want to show a progress indicator at + /// the top of the screen. + final bool progressIndicator; + + /// Localization for the order detail screen. + final OrderDetailLocalization localization; + + /// Padding around the input fields. + final EdgeInsets inputFieldPadding; + + /// Padding around the title of the input fields. + final EdgeInsets titlePadding; + + /// Optional app bar that you can pass to the order detail screen. + final PreferredSizeWidget? appBar; +} diff --git a/packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart b/packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart new file mode 100644 index 0000000..f339d3a --- /dev/null +++ b/packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart @@ -0,0 +1,18 @@ +/// Localizations for the order detail page. +class OrderDetailLocalization { + /// Constructor for the order detail localization. + const OrderDetailLocalization({ + this.nextButton = "Next", + this.backButton = "Back", + this.completeButton = "Complete", + }); + + /// Next button localization. + final String nextButton; + + /// Back button localization. + final String backButton; + + /// Complete button localization. + final String completeButton; +} diff --git a/packages/flutter_order_details/lib/src/configuration/order_detail_step.dart b/packages/flutter_order_details/lib/src/configuration/order_detail_step.dart new file mode 100644 index 0000000..feb0658 --- /dev/null +++ b/packages/flutter_order_details/lib/src/configuration/order_detail_step.dart @@ -0,0 +1,22 @@ +import "package:flutter/widgets.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; + +/// Configuration for the order detail step. +class OrderDetailStep { + /// Constructor for the order detail step. + OrderDetailStep({ + required this.formKey, + required this.fields, + this.stepName, + }); + + /// Optional name for the step. + final String? stepName; + + /// Form key for the step. + final GlobalKey formKey; + + /// List of fields that the user has to fill in. + /// Each field must extend from the `OrderDetailInput` class. + final List fields; +} diff --git a/packages/flutter_order_details/lib/src/configuration/order_detail_title_style.dart b/packages/flutter_order_details/lib/src/configuration/order_detail_title_style.dart new file mode 100644 index 0000000..8cc7896 --- /dev/null +++ b/packages/flutter_order_details/lib/src/configuration/order_detail_title_style.dart @@ -0,0 +1,14 @@ +/// An enum to define the style of the title in the order detail. +enum OrderDetailTitleStyle { + /// The title displayed as a textlabel above the field. + text, + + /// The title displayed as a label inside the field. + /// NOTE: Not all fields support this. Such as, but not limited to: + /// - Dropdown + /// - Time Picker + label, + + /// Does not display any form of title. + none, +} diff --git a/packages/flutter_order_details/lib/src/models/formfield_error_builder.dart b/packages/flutter_order_details/lib/src/models/formfield_error_builder.dart new file mode 100644 index 0000000..b764eed --- /dev/null +++ b/packages/flutter_order_details/lib/src/models/formfield_error_builder.dart @@ -0,0 +1,25 @@ +import "package:flutter/material.dart"; + +/// Error Builder for form fields. +class FormFieldErrorBuilder extends StatelessWidget { + /// Constructor for the form field error builder. + const FormFieldErrorBuilder({ + required this.errorMessage, + super.key, + }); + + /// Error message to display. + final String errorMessage; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Text( + errorMessage, + textAlign: TextAlign.left, + style: TextStyle( + color: theme.colorScheme.error, + ), + ); + } +} diff --git a/packages/flutter_order_details/lib/src/models/order_address_input.dart b/packages/flutter_order_details/lib/src/models/order_address_input.dart new file mode 100644 index 0000000..e34f4cc --- /dev/null +++ b/packages/flutter_order_details/lib/src/models/order_address_input.dart @@ -0,0 +1,160 @@ +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; + +/// Order input for addresses with with predefined text fields and validation. +class OrderAddressInput extends OrderDetailInput { + /// Constructor of the order address input. + OrderAddressInput({ + required super.title, + required super.outputKey, + required this.textController, + super.titleStyle, + super.titleAlignment, + super.titlePadding, + super.subtitle, + super.errorIsRequired, + super.hint = "0000XX", + super.isRequired, + super.isReadOnly, + super.initialValue, + this.streetNameTitle = "Street name", + this.postalCodeTitle = "Postal code", + this.cityTitle = "City", + this.streetNameValidators, + this.postalCodeValidators, + this.cityValidators, + this.inputFormatters, + super.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 4), + }); + + /// Title for the street name. + final String streetNameTitle; + + /// Title for the postal code. + final String postalCodeTitle; + + /// Title for the city. + final String cityTitle; + + /// Text Control parent that contains the value of all the other three + /// controllers. + final TextEditingController textController; + + /// Text Controller for street names. + final TextEditingController streetNameController = TextEditingController(); + + /// Text Controller for postal codes. + final TextEditingController postalCodeController = TextEditingController(); + + /// Text Controller for the city name. + final TextEditingController cityController = TextEditingController(); + + /// Validators for the street name. + final List? streetNameValidators; + + /// Validators for the postal code. + final List? postalCodeValidators; + + /// Validators for the city. + final List? cityValidators; + + /// Input formatters for the postal code. + final List? inputFormatters; + + @override + Widget build( + BuildContext context, + String? buildInitialValue, + Function({bool needsBlur}) onBlurBackground, + ) { + void setUpControllers(String address) { + var addressParts = address.split(", "); + + if (addressParts.isNotEmpty) { + streetNameController.text = addressParts[0]; + } + + if (addressParts.length > 1) { + postalCodeController.text = addressParts[1]; + } + + if (addressParts.length > 2) { + cityController.text = addressParts[2]; + } + } + + void inputChanged(String _) { + var address = "${streetNameController.text}, " + "${postalCodeController.text}, " + "${cityController.text}"; + + textController.text = address; + + currentValue = address; + onValueChanged?.call(address); + } + + textController.text = initialValue ?? buildInitialValue ?? ""; + currentValue = textController.text; + + setUpControllers(currentValue ?? ""); + + return buildOutline( + context, + [ + OrderTextInput( + title: streetNameTitle, + outputKey: "internal_street_name", + textController: streetNameController, + titleStyle: OrderDetailTitleStyle.none, + onValueChanged: inputChanged, + hint: "De Dam 1", + initialValue: streetNameController.text, + validators: streetNameValidators ?? [], + ), + OrderTextInput( + title: postalCodeTitle, + outputKey: "internal_postal_code", + textController: postalCodeController, + titleStyle: OrderDetailTitleStyle.none, + onValueChanged: inputChanged, + validators: postalCodeValidators ?? + [ + (value) { + if (value?.length != 6) { + return "Postal code must be 6 characters"; + } + return null; + }, + (value) { + if (value != null && + !RegExp(r"^\d{4}\s?[a-zA-Z]{2}$").hasMatch(value)) { + return "Postal code must be in the format 0000XX"; + } + return null; + } + ], + inputFormatters: inputFormatters ?? + [ + FilteringTextInputFormatter.allow(RegExp(r"^\d{0,4}[A-Z]*")), + LengthLimitingTextInputFormatter(6), + ], + hint: hint, + initialValue: postalCodeController.text, + ), + OrderTextInput( + title: cityTitle, + outputKey: "internal_city", + textController: cityController, + titleStyle: OrderDetailTitleStyle.none, + onValueChanged: inputChanged, + hint: "Amsterdam", + initialValue: cityController.text, + validators: cityValidators ?? [], + ), + ], + onBlurBackground, + ); + } +} diff --git a/packages/flutter_order_details/lib/src/models/order_choice_input.dart b/packages/flutter_order_details/lib/src/models/order_choice_input.dart new file mode 100644 index 0000000..f482233 --- /dev/null +++ b/packages/flutter_order_details/lib/src/models/order_choice_input.dart @@ -0,0 +1,175 @@ +import "package:flutter/material.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; +import "package:flutter_order_details/src/models/formfield_error_builder.dart"; + +/// Order input for choice with predefined text fields and validation. +class OrderChoiceInput extends OrderDetailInput { + /// Constructor of the order choice input. + OrderChoiceInput({ + required super.title, + required super.outputKey, + required this.items, + super.titleStyle, + super.titleAlignment, + super.titlePadding, + super.subtitle, + super.errorIsRequired, + super.isRequired, + super.isReadOnly, + super.initialValue, + this.fieldHeight = 140, + this.fieldPadding = const EdgeInsets.symmetric( + horizontal: 4, + vertical: 64, + ), + this.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 12), + }); + + /// Items to show within the dropdown menu. + final List items; + + /// Padding for the field. + final EdgeInsets fieldPadding; + + /// Padding between fields. + @override + // ignore: overridden_fields + final EdgeInsets paddingBetweenFields; + + /// The height of the input field. + final double fieldHeight; + + final _ChoiceNotifier _notifier = _ChoiceNotifier(); + + @override + Widget build( + BuildContext context, + String? buildInitialValue, + Function({bool needsBlur}) onBlurBackground, + ) { + void onItemChanged(String value) { + if (value == currentValue) { + currentValue = null; + onValueChanged?.call(""); + _notifier.setValue(""); + } else { + currentValue = value; + onValueChanged?.call(value); + _notifier.setValue(value); + } + } + + return buildOutline( + context, + ListenableBuilder( + listenable: _notifier, + builder: (context, child) => _ChoiceInputField( + currentValue: currentValue ?? initialValue ?? buildInitialValue ?? "", + items: items, + onTap: onItemChanged, + validate: validate, + fieldPadding: fieldPadding, + paddingBetweenFields: paddingBetweenFields, + ), + ), + onBlurBackground, + ); + } +} + +class _ChoiceNotifier extends ChangeNotifier { + String? _value; + + String? get value => _value; + + void setValue(String value) { + _value = value; + notifyListeners(); + } +} + +class _ChoiceInputField extends FormField { + _ChoiceInputField({ + required T currentValue, + required List items, + required Function(T) onTap, + required String? Function(T?) validate, + required EdgeInsets fieldPadding, + required EdgeInsets paddingBetweenFields, + super.key, + }) : super( + validator: (value) => validate(currentValue), + builder: (FormFieldState field) => Padding( + padding: fieldPadding, + child: Column( + children: [ + for (var item in items) ...[ + Padding( + padding: paddingBetweenFields, + child: _InputContent( + i: item, + currentValue: currentValue, + onTap: onTap, + ), + ), + ], + if (field.hasError) ...[ + FormFieldErrorBuilder(errorMessage: field.errorText!), + ], + ], + ), + ), + ); +} + +class _InputContent extends StatelessWidget { + const _InputContent({ + required this.i, + required this.currentValue, + required this.onTap, + }); + + final T i; + final T currentValue; + final Function(T) onTap; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var boxDecoration = BoxDecoration( + color: currentValue == i.toString() + ? theme.colorScheme.primary + : theme.colorScheme.secondary, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: theme.colorScheme.primary, + width: 1, + ), + ); + + var decoratedBox = Container( + decoration: boxDecoration, + width: double.infinity, + height: 150, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + i.toString(), + style: theme.textTheme.labelLarge?.copyWith( + color: currentValue == i.toString() + ? theme.colorScheme.onPrimary + : theme.colorScheme.primary, + ), + ), + ], + ), + ); + + return GestureDetector( + onTap: () => onTap(i), + child: decoratedBox, + ); + } +} diff --git a/packages/flutter_order_details/lib/src/models/order_dropdown_input.dart b/packages/flutter_order_details/lib/src/models/order_dropdown_input.dart new file mode 100644 index 0000000..640099e --- /dev/null +++ b/packages/flutter_order_details/lib/src/models/order_dropdown_input.dart @@ -0,0 +1,153 @@ +import "package:flutter/material.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; + +/// Order Detail input for a dropdown input. +class OrderDropdownInput extends OrderDetailInput { + /// Constructor for the order dropdown input. + OrderDropdownInput({ + required super.title, + required super.outputKey, + required this.items, + super.titleStyle, + super.titleAlignment, + super.titlePadding, + super.subtitle, + super.errorIsRequired, + super.isRequired = true, + super.isReadOnly, + super.initialValue, + this.blurOnInteraction = true, + }); + + /// Items to show within the dropdown menu. + final List items; + + /// Whether or not the screen should blur when interacting. + final bool blurOnInteraction; + + @override + Widget build( + BuildContext context, + T? buildInitialValue, + Function({bool needsBlur}) onBlurBackground, + ) { + var theme = Theme.of(context); + + void onItemChanged(T? value) { + currentValue = value; + onValueChanged?.call(value as T); + onBlurBackground(needsBlur: false); + } + + void onPopupOpen() { + if (blurOnInteraction) + onBlurBackground( + needsBlur: true, + ); + } + + var inputDecoration = InputDecoration( + labelText: titleStyle == OrderDetailTitleStyle.label ? title : null, + hintText: hint, + filled: true, + fillColor: theme.inputDecorationTheme.fillColor, + border: InputBorder.none, + ); + + currentValue = + currentValue ?? initialValue ?? buildInitialValue ?? items[0]; + + return buildOutline( + context, + DropdownButtonFormField( + value: currentValue ?? initialValue ?? buildInitialValue ?? items[0], + selectedItemBuilder: (context) => items + .map( + (item) => Text( + item.toString(), + style: theme.textTheme.labelMedium, + ), + ) + .toList(), + items: items + .map( + (item) => DropdownMenuItem( + value: item, + child: _DropdownButtonBuilder( + item: item, + currentValue: currentValue, + ), + ), + ) + .toList(), + onChanged: onItemChanged, + onTap: onPopupOpen, + style: theme.textTheme.labelMedium, + decoration: inputDecoration, + borderRadius: BorderRadius.circular(10), + icon: const Icon(Icons.keyboard_arrow_down_sharp), + validator: super.validate, + ), + onBlurBackground, + ); + } +} + +class _DropdownButtonBuilder extends StatelessWidget { + const _DropdownButtonBuilder({ + required this.item, + this.currentValue, + super.key, + }); + + final T item; + final T? currentValue; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var textBuilder = Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + item.toString(), + style: theme.textTheme.labelMedium?.copyWith( + color: item == currentValue ? theme.colorScheme.onPrimary : null, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + + var selectedIcon = Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + Icons.check, + color: theme.colorScheme.onPrimary, + ), + ), + ); + + return DecoratedBox( + decoration: BoxDecoration( + color: item == currentValue ? theme.colorScheme.primary : null, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: theme.colorScheme.primary, + ), + ), + child: Stack( + children: [ + textBuilder, + if (currentValue == item) ...[ + selectedIcon, + ], + ], + ), + ); + } +} diff --git a/packages/flutter_order_details/lib/src/models/order_email_input.dart b/packages/flutter_order_details/lib/src/models/order_email_input.dart new file mode 100644 index 0000000..a2bf053 --- /dev/null +++ b/packages/flutter_order_details/lib/src/models/order_email_input.dart @@ -0,0 +1,75 @@ +import "package:flutter/material.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; + +/// Order Email input with predefined validators. +class OrderEmailInput extends OrderDetailInput { + /// Constructor of the order email input. + OrderEmailInput({ + required super.title, + required super.outputKey, + required this.textController, + super.titleStyle, + super.titleAlignment, + super.titlePadding, + super.subtitle, + super.hint, + super.errorIsRequired, + super.isRequired, + super.isReadOnly, + super.initialValue, + this.errorInvalidEmail = "Invalid email ( your_name@example.com )", + }) : super( + validators: [ + (value) { + if (value != null && !RegExp(r"^\w+@\w+\.\w+$").hasMatch(value)) { + return errorInvalidEmail; + } + return null; + }, + ], + ); + + /// Text Controller for email input. + final TextEditingController textController; + + /// Error message for invalid email. + final String errorInvalidEmail; + + @override + Widget build( + BuildContext context, + String? buildInitialValue, + Function({bool needsBlur}) onBlurBackground, + ) { + var theme = Theme.of(context); + + textController.text = initialValue ?? buildInitialValue ?? ""; + currentValue = textController.text; + + return buildOutline( + context, + TextFormField( + style: theme.textTheme.labelMedium, + controller: textController, + onChanged: (String value) { + currentValue = value; + super.onValueChanged?.call(value); + }, + decoration: InputDecoration( + labelText: titleStyle == OrderDetailTitleStyle.label ? title : null, + hintText: hint, + filled: true, + fillColor: theme.inputDecorationTheme.fillColor, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + ), + validator: (value) => super.validate(value), + keyboardType: TextInputType.emailAddress, + readOnly: isReadOnly, + ), + onBlurBackground, + ); + } +} diff --git a/packages/flutter_order_details/lib/src/models/order_input.dart b/packages/flutter_order_details/lib/src/models/order_input.dart new file mode 100644 index 0000000..d3ac019 --- /dev/null +++ b/packages/flutter_order_details/lib/src/models/order_input.dart @@ -0,0 +1,173 @@ +import "package:flutter/material.dart"; +import "package:flutter_order_details/src/configuration/order_detail_title_style.dart"; + +/// Abstract class for order detail input. +/// Each input field must extend from this class. +abstract class OrderDetailInput { + /// Constructor for the order detail input. + OrderDetailInput({ + required this.title, + required this.outputKey, + this.titleStyle = OrderDetailTitleStyle.text, + this.titleAlignment = Alignment.centerLeft, + this.titlePadding = const EdgeInsets.symmetric(vertical: 4), + this.subtitle, + this.isRequired = true, + this.isReadOnly = false, + this.initialValue, + this.validators = const [], + this.onValueChanged, + this.hint, + this.errorIsRequired = "This field is required", + this.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 4), + }); + + /// Title of the input field. + final String title; + + /// Subtitle of the input field. + final String? subtitle; + + /// The styling for the title. + final OrderDetailTitleStyle titleStyle; + + /// The alignment of the titl + final Alignment titleAlignment; + + /// Padding around the title. + final EdgeInsets titlePadding; + + /// The output key of the input field. + final String outputKey; + + /// Hint message of the input field. + final String? hint; + + /// Determines if the input field is required. + final bool isRequired; + + /// Error message for when an user does not insert something in the field + /// even though it is required. + final String errorIsRequired; + + /// A read-only field that users cannot change. + final bool isReadOnly; + + /// An initial value for the input field. This is ideal incombination + /// with the [isReadOnly] field. + final T? initialValue; + + /// Internal current value. Do not use. + T? currentValue; + + /// List of validators that should be executed when the input field + /// is validated. + List validators; + + /// Function that is called when the value of the input field changes. + final Function(T)? onValueChanged; + + /// Padding between the fields. + final EdgeInsets paddingBetweenFields; + + /// Allows you to update the current value. + @protected + set updateValue(T value) { + currentValue = value; + } + + /// Function that validates the input field. Automatically keeps track + /// of the [isRequired] keys and all the custom validators. + @protected + String? validate(T? value) { + if (isRequired && (value == null || value.toString().isEmpty)) { + return errorIsRequired; + } + + for (var validator in validators) { + var error = validator(value); + if (error != null) { + return error; + } + } + + return null; + } + + /// Builds the basic outline of an input field. + @protected + Widget buildOutline( + BuildContext context, + // ignore: avoid_annotating_with_dynamic + dynamic child, + Function({bool needsBlur}) onBlurBackground, + ) { + var theme = Theme.of(context); + + return Column( + children: [ + if (titleStyle == OrderDetailTitleStyle.text) ...[ + Align( + alignment: titleAlignment, + child: Padding( + padding: titlePadding, + child: Text( + title, + style: theme.textTheme.titleMedium, + ), + ), + ), + if (subtitle != null) ...[ + Padding( + padding: titlePadding, + child: Align( + alignment: titleAlignment, + child: Text( + subtitle!, + style: theme.textTheme.titleSmall, + ), + ), + ), + ], + ], + if (child is FormField || child is Widget) ...[ + child, + ] else if (child is List) ...[ + Column( + children: child + .map( + (FormField field) => Padding( + padding: paddingBetweenFields, + child: field, + ), + ) + .toList(), + ), + ] else if (child is List) ...[ + Column( + children: child + .map( + (OrderDetailInput input) => Padding( + padding: paddingBetweenFields, + child: input.build( + context, + input.initialValue, + onBlurBackground, + ), + ), + ) + .toList(), + ), + ], + ], + ); + } + + /// Abstract build function that each orderinput class must implement + /// themsleves. For a basic layout, they can use the [buildOutline] function. + Widget build( + BuildContext context, + T? buildInitialValue, + Function({bool needsBlur}) onBlurBackground, + ); +} diff --git a/packages/flutter_order_details/lib/src/models/order_phone_input.dart b/packages/flutter_order_details/lib/src/models/order_phone_input.dart new file mode 100644 index 0000000..f6afb3a --- /dev/null +++ b/packages/flutter_order_details/lib/src/models/order_phone_input.dart @@ -0,0 +1,101 @@ +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; + +/// Order input for phone numbers with with predefined +/// text fields and validation. +class OrderPhoneInput extends OrderDetailInput { + /// Constructor for the phone input. + OrderPhoneInput({ + required super.title, + required super.outputKey, + required this.textController, + this.errorMustBe11Digits = "Number must be 11 digits (+31 6 XXXX XXXX)", + this.errorMustStartWith316 = "Number must start with +316", + this.errorMustBeNumeric = "Number must be numeric", + super.errorIsRequired, + super.subtitle, + super.titleAlignment, + super.titlePadding, + super.titleStyle, + super.isRequired, + super.isReadOnly, + super.initialValue, + }) : super( + validators: [ + (value) { + if (value != null && value.length != 11) { + return errorMustBe11Digits; + } + return null; + }, + (value) { + if (value != null && !value.startsWith("316")) { + return errorMustStartWith316; + } + return null; + }, + (value) { + if (value != null && !RegExp(r"^\d+$").hasMatch(value)) { + return errorMustBeNumeric; + } + return null; + }, + ], + ); + + /// Text Controller for phone input. + final TextEditingController textController; + + /// Error message that notifies the number must be 11 digits long. + final String errorMustBe11Digits; + + /// Error message that notifies the number must start with +316 + final String errorMustStartWith316; + + /// Error message that notifies the number must be numeric. + final String errorMustBeNumeric; + + @override + Widget build( + BuildContext context, + String? buildInitialValue, + Function({bool needsBlur}) onBlurBackground, + ) { + var theme = Theme.of(context); + + textController.text = initialValue ?? buildInitialValue ?? "31"; + currentValue = textController.text; + + return buildOutline( + context, + TextFormField( + style: theme.textTheme.labelMedium, + controller: textController, + onChanged: (String value) { + currentValue = value; + super.onValueChanged?.call(value); + }, + decoration: InputDecoration( + labelText: titleStyle == OrderDetailTitleStyle.label ? title : null, + prefixText: "+", + prefixStyle: theme.textTheme.labelMedium, + hintText: hint, + filled: true, + fillColor: theme.inputDecorationTheme.fillColor, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + ), + validator: (value) => super.validate(value), + readOnly: isReadOnly, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(11), // international phone number + ], + ), + onBlurBackground, + ); + } +} diff --git a/packages/flutter_order_details/lib/src/models/order_result.dart b/packages/flutter_order_details/lib/src/models/order_result.dart new file mode 100644 index 0000000..20a4c00 --- /dev/null +++ b/packages/flutter_order_details/lib/src/models/order_result.dart @@ -0,0 +1,14 @@ +/// OrderResult model. +/// When an user completes the field and presses the complete button, +/// the `onComplete` method returns an instance of this class that contains +/// all the developer-specified `outputKey`s and the value that was provided +/// by the user. +class OrderResult { + /// Constructor of the order result class. + OrderResult({ + required this.order, + }); + + /// Map of `outputKey`s and their respected values. + final Map order; +} diff --git a/packages/flutter_order_details/lib/src/models/order_text_input.dart b/packages/flutter_order_details/lib/src/models/order_text_input.dart new file mode 100644 index 0000000..5967160 --- /dev/null +++ b/packages/flutter_order_details/lib/src/models/order_text_input.dart @@ -0,0 +1,71 @@ +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; + +/// Default text input for order details. +class OrderTextInput extends OrderDetailInput { + /// Default text input for order details. + OrderTextInput({ + required super.title, + required super.outputKey, + required this.textController, + super.titleStyle, + super.titleAlignment, + super.titlePadding, + super.subtitle, + super.isRequired, + super.isReadOnly, + super.initialValue, + super.validators, + super.onValueChanged, + super.errorIsRequired, + super.hint, + this.inputFormatters = const [], + }); + + /// Text Controller for the input field. + final TextEditingController textController; + + /// List of input formatters for the text field. + final List inputFormatters; + + @override + Widget build( + BuildContext context, + String? buildInitialValue, + Function({bool needsBlur}) onBlurBackground, + ) { + var theme = Theme.of(context); + + textController.text = initialValue ?? buildInitialValue ?? ""; + currentValue = textController.text; + + return buildOutline( + context, + TextFormField( + style: theme.textTheme.labelMedium, + controller: textController, + onChanged: (String value) { + currentValue = value; + super.onValueChanged?.call(value); + }, + decoration: InputDecoration( + labelText: titleStyle == OrderDetailTitleStyle.label ? title : null, + hintText: hint, + filled: true, + fillColor: theme.inputDecorationTheme.fillColor, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + ), + validator: super.validate, + readOnly: isReadOnly, + inputFormatters: [ + ...inputFormatters, + ], + ), + onBlurBackground, + ); + } +} diff --git a/packages/flutter_order_details/lib/src/models/order_time_picker_input.dart b/packages/flutter_order_details/lib/src/models/order_time_picker_input.dart new file mode 100644 index 0000000..b8ef05f --- /dev/null +++ b/packages/flutter_order_details/lib/src/models/order_time_picker_input.dart @@ -0,0 +1,353 @@ +import "package:flutter/material.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; +import "package:flutter_order_details/src/models/formfield_error_builder.dart"; + +/// Order time picker input with predefined text fields and validation. +class OrderTimePicker extends OrderDetailInput { + /// Constructor for the time picker. + OrderTimePicker({ + required super.title, + required super.outputKey, + super.titleStyle, + super.titleAlignment, + super.titlePadding, + super.subtitle, + super.isRequired, + super.initialValue, + super.validators, + super.onValueChanged, + super.errorIsRequired, + super.hint, + this.beginTime = 9, + this.endTime = 17, + this.interval = 0.25, + this.morningLabel = "Morning", + this.afternoonLabel = "Afternoon", + this.eveningLabel = "Evening", + this.padding = const EdgeInsets.only(top: 12, bottom: 20.0), + }) : assert( + beginTime < endTime, + "Begin time cannot be greater than end time", + ); + + /// Minimum time of times to show. For example 9 (for 9AM). + final double beginTime; + + /// Final time to show. For example 17 (for 5PM). + final double endTime; + + /// For each interval a button gets generated within the begin time and + /// the end time. For example 0.25 (for ever 15 minutes). + final double interval; + + /// Translation for morning texts. + final String morningLabel; + + /// Translation for afternoon texts. + final String afternoonLabel; + + /// Translation for evening texts. + final String eveningLabel; + + /// Padding around the time picker. + final EdgeInsets padding; + + final _selectedTimeOfDay = _SelectedTimeOfDay(); + + @override + Widget build( + BuildContext context, + String? buildInitialValue, + Function({bool needsBlur}) onBlurBackground, + ) { + void updateSelectedTimeOfDay(_TimeOfDay timeOfDay) { + if (_selectedTimeOfDay.selectedTimeOfDay == timeOfDay) return; + _selectedTimeOfDay.selectedTimeOfDay = timeOfDay; + currentValue = null; + } + + void updateSelectedTimeAsString(String? time) { + currentValue = time; + onValueChanged?.call(time ?? ""); + _selectedTimeOfDay.selectedTime = time; + } + + void updateSelectedTime(double time) { + if (currentValue == time.toString()) { + updateSelectedTimeAsString(null); + } else { + updateSelectedTimeAsString(time.toString()); + } + } + + if (currentValue != null) { + var currentValueAsDouble = double.parse(currentValue!); + for (var timeOfDay in _TimeOfDay.values) { + if (_isTimeWithinTimeOfDay( + currentValueAsDouble, + currentValueAsDouble, + timeOfDay, + )) { + _selectedTimeOfDay.selectedTimeOfDay = timeOfDay; + } + } + updateSelectedTimeAsString(currentValue); + } else { + for (var timeOfDay in _TimeOfDay.values) { + if (_isTimeWithinTimeOfDay(beginTime, endTime, timeOfDay)) { + _selectedTimeOfDay.selectedTimeOfDay = timeOfDay; + break; + } + } + } + + return buildOutline( + context, + ListenableBuilder( + listenable: _selectedTimeOfDay, + builder: (context, _) { + var startTime = _selectedTimeOfDay.selection != null + ? _selectedTimeOfDay.selection!.minTime.clamp(beginTime, endTime) + : beginTime; + var finalTime = _selectedTimeOfDay.selection != null + ? _selectedTimeOfDay.selection!.maxTime.clamp(beginTime, endTime) + : endTime; + + return Column( + children: [ + _TimeOfDaySelector( + selectedTimeOfDay: _selectedTimeOfDay, + updateSelectedTimeOfDay: updateSelectedTimeOfDay, + startTime: beginTime, + endTime: endTime, + morningLabel: morningLabel, + afternoonLabel: afternoonLabel, + eveningLabel: eveningLabel, + padding: padding, + ), + _TimeWrap( + currentValue: currentValue ?? "", + startTime: startTime, + finalTime: finalTime, + interval: interval, + onTap: updateSelectedTime, + validate: super.validate, + ), + ], + ); + }, + ), + onBlurBackground, + ); + } +} + +bool _isTimeWithinTimeOfDay( + double openingTime, + double closingTime, + _TimeOfDay timeOfDay, +) => + (timeOfDay.minTime >= openingTime && timeOfDay.minTime <= closingTime) || + (timeOfDay.maxTime > openingTime && timeOfDay.maxTime <= closingTime) || + (timeOfDay.minTime <= openingTime && timeOfDay.maxTime >= closingTime); + +class _TimeOfDaySelector extends StatelessWidget { + const _TimeOfDaySelector({ + required this.selectedTimeOfDay, + required this.updateSelectedTimeOfDay, + required this.startTime, + required this.endTime, + required this.morningLabel, + required this.afternoonLabel, + required this.eveningLabel, + required this.padding, + }); + + final _SelectedTimeOfDay selectedTimeOfDay; + final Function(_TimeOfDay) updateSelectedTimeOfDay; + final double startTime; + final double endTime; + final String morningLabel; + final String afternoonLabel; + final String eveningLabel; + final EdgeInsets padding; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + String getLabelName(_TimeOfDay timeOfDay) => switch (timeOfDay) { + _TimeOfDay.morning => morningLabel, + _TimeOfDay.afternoon => afternoonLabel, + _TimeOfDay.evening => eveningLabel, + }; + + return Padding( + padding: padding, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(90), + border: Border.all( + color: theme.colorScheme.primary, + strokeAlign: BorderSide.strokeAlignOutside, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (var timeOfDay in _TimeOfDay.values) ...[ + if (_isTimeWithinTimeOfDay(startTime, endTime, timeOfDay)) ...[ + GestureDetector( + onTap: () => updateSelectedTimeOfDay(timeOfDay), + child: DecoratedBox( + decoration: BoxDecoration( + color: selectedTimeOfDay.selectedTimeOfDay == timeOfDay + ? theme.colorScheme.primary + : Colors.white, + borderRadius: BorderRadius.circular(90), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, + ), + child: Text( + getLabelName(timeOfDay), + style: theme.textTheme.labelMedium?.copyWith( + color: + selectedTimeOfDay.selectedTimeOfDay == timeOfDay + ? Colors.white + : theme.colorScheme.primary, + ), + ), + ), + ), + ), + ], + ], + ], + ), + ), + ); + } +} + +class _TimeWrap extends FormField { + _TimeWrap({ + required this.currentValue, + required this.startTime, + required this.finalTime, + required this.interval, + required this.onTap, + required String? Function(T?) validate, + }) : super( + validator: (value) => validate(currentValue), + builder: (FormFieldState field) => Column( + children: [ + Wrap( + children: [ + for (var i = startTime; i < finalTime; i += interval) ...[ + _TimeWrapContent( + i: i, + currentValue: currentValue, + onTap: onTap, + ), + ], + ], + ), + if (field.hasError) ...[ + FormFieldErrorBuilder(errorMessage: field.errorText!), + ], + ], + ), + ); + + final T currentValue; + final double startTime; + final double finalTime; + final double interval; + final Function(double) onTap; +} + +class _TimeWrapContent extends StatelessWidget { + const _TimeWrapContent({ + required this.i, + required this.currentValue, + required this.onTap, + }); + + final double i; + final T currentValue; + final Function(double) onTap; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var boxDecoration = BoxDecoration( + color: currentValue == i.toString() + ? theme.colorScheme.primary + : Colors.white, + borderRadius: BorderRadius.circular(16), + ); + + var decoratedBox = Container( + decoration: boxDecoration, + width: MediaQuery.of(context).size.width * .25, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 28, + vertical: 12, + ), + child: Text( + '${i.floor().toString().padLeft(2, '0')}:' + '${((i - i.floor()) * 60).toInt().toString().padLeft(2, '0')}', + style: theme.textTheme.labelMedium?.copyWith( + color: currentValue == i.toString() + ? Colors.white + : theme.colorScheme.primary, + ), + ), + ), + ); + + return GestureDetector( + onTap: () => onTap(i), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: decoratedBox, + ), + ); + } +} + +class _SelectedTimeOfDay extends ChangeNotifier { + _TimeOfDay? selection; + String? time = ""; + + _TimeOfDay? get selectedTimeOfDay => selection; + String? get selectedTime => time; + + set selectedTimeOfDay(_TimeOfDay? value) { + selection = value; + notifyListeners(); + } + + set selectedTime(String? value) { + time = value; + notifyListeners(); + } +} + +enum _TimeOfDay { + morning(0, 12), + afternoon(12, 18), + evening(18, 24); + + const _TimeOfDay(this.minTime, this.maxTime); + + final double minTime; + final double maxTime; +} diff --git a/packages/flutter_order_details/lib/src/widgets/order_detail_screen.dart b/packages/flutter_order_details/lib/src/widgets/order_detail_screen.dart new file mode 100644 index 0000000..0304e0b --- /dev/null +++ b/packages/flutter_order_details/lib/src/widgets/order_detail_screen.dart @@ -0,0 +1,273 @@ +import "package:flutter/material.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; + +/// Order Detail Screen. +class OrderDetailScreen extends StatefulWidget { + /// Screen that builds all forms based on the configuration. + const OrderDetailScreen({ + required this.configuration, + super.key, + }); + + /// Configuration for the screen. + final OrderDetailConfiguration configuration; + + @override + State createState() => _OrderDetailScreenState(); +} + +class _OrderDetailScreenState extends State { + final _CurrentStep _currentStep = _CurrentStep(); + + final OrderResult _orderResult = OrderResult(order: {}); + + bool _blurBackground = false; + + void _toggleBlurBackground({bool? needsBlur}) { + setState(() { + _blurBackground = needsBlur!; + }); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var pageBody = SafeArea( + left: false, + right: false, + bottom: true, + child: _OrderDetailBody( + configuration: widget.configuration, + orderResult: _orderResult, + currentStep: _currentStep, + onBlurBackground: _toggleBlurBackground, + ), + ); + + var pageBlur = GestureDetector( + onTap: () => _toggleBlurBackground(needsBlur: false), + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: theme.colorScheme.surface.withOpacity(0.5), + ), + ), + ); + + return Scaffold( + appBar: widget.configuration.appBar, + body: Stack( + children: [ + pageBody, + if (_blurBackground) pageBlur, + ], + ), + ); + } +} + +class _CurrentStep extends ChangeNotifier { + int _step = 0; + + int get step => _step; + + void increment() { + _step++; + notifyListeners(); + } + + void decrement() { + _step--; + notifyListeners(); + } +} + +class _OrderDetailBody extends StatelessWidget { + const _OrderDetailBody({ + required this.configuration, + required this.orderResult, + required this.currentStep, + required this.onBlurBackground, + }); + + final OrderDetailConfiguration configuration; + final OrderResult orderResult; + final _CurrentStep currentStep; + final Function({bool needsBlur}) onBlurBackground; + + @override + Widget build(BuildContext context) => ListenableBuilder( + listenable: currentStep, + builder: (context, _) => Builder( + builder: (context) => _FormBuilder( + currentStep: currentStep, + orderResult: orderResult, + configuration: configuration, + onBlurBackground: onBlurBackground, + ), + ), + ); +} + +class _FormBuilder extends StatelessWidget { + const _FormBuilder({ + required this.currentStep, + required this.configuration, + required this.orderResult, + required this.onBlurBackground, + }); + + final _CurrentStep currentStep; + final OrderDetailConfiguration configuration; + final OrderResult orderResult; + + final Function({bool needsBlur}) onBlurBackground; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var progressIndicator = LinearProgressIndicator( + value: currentStep.step / configuration.steps.length, + backgroundColor: theme.colorScheme.surface, + ); + + var stepForm = Form( + key: configuration.steps[currentStep.step].formKey, + child: _StepBuilder( + configuration: configuration, + currentStep: configuration.steps[currentStep.step], + orderResult: orderResult, + theme: theme, + onBlurBackground: onBlurBackground, + ), + ); + + void onPressedNext() { + var formInfo = configuration.steps[currentStep.step]; + var formkey = formInfo.formKey; + for (var input in formInfo.fields) { + orderResult.order[input.outputKey] = input.currentValue; + } + + if (formkey.currentState!.validate()) { + currentStep.increment(); + } + } + + void onPressedPrevious() { + var formInfo = configuration.steps[currentStep.step]; + for (var input in formInfo.fields) { + orderResult.order[input.outputKey] = input.currentValue; + } + + currentStep.decrement(); + } + + void onPressedComplete() { + var formInfo = configuration.steps[currentStep.step]; + var formkey = formInfo.formKey; + for (var input in formInfo.fields) { + orderResult.order[input.outputKey] = input.currentValue; + } + + if (formkey.currentState!.validate()) { + configuration.onCompleted(orderResult); + } + } + + var navigationControl = Row( + children: [ + if (currentStep.step > 0) ...[ + TextButton( + onPressed: onPressedPrevious, + child: Text( + configuration.localization.backButton, + ), + ), + ], + const Spacer(), + if (currentStep.step < configuration.steps.length - 1) ...[ + TextButton( + onPressed: onPressedNext, + child: Text( + configuration.localization.nextButton, + ), + ), + ] else ...[ + TextButton( + onPressed: onPressedComplete, + child: Text( + configuration.localization.completeButton, + ), + ), + ], + ], + ); + + return Stack( + children: [ + SingleChildScrollView( + child: Column( + children: [ + if (configuration.progressIndicator) ...[ + progressIndicator, + ], + stepForm, + ], + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: navigationControl, + ), + ], + ); + } +} + +class _StepBuilder extends StatelessWidget { + const _StepBuilder({ + required this.configuration, + required this.currentStep, + required this.orderResult, + required this.theme, + required this.onBlurBackground, + }); + + final OrderDetailConfiguration configuration; + final OrderDetailStep currentStep; + final OrderResult orderResult; + final ThemeData theme; + final Function({bool needsBlur}) onBlurBackground; + + @override + Widget build(BuildContext context) { + var title = currentStep.stepName != null + ? Padding( + padding: configuration.titlePadding, + child: Text( + currentStep.stepName!, + style: theme.textTheme.titleMedium, + ), + ) + : const SizedBox.shrink(); + + return Column( + children: [ + title, + for (var input in currentStep.fields) + Padding( + padding: configuration.inputFieldPadding, + child: input.build( + context, + orderResult.order[input.outputKey], + onBlurBackground, + ), + ), + ], + ); + } +} diff --git a/packages/flutter_order_details/pubspec.yaml b/packages/flutter_order_details/pubspec.yaml new file mode 100644 index 0000000..7d37a9f --- /dev/null +++ b/packages/flutter_order_details/pubspec.yaml @@ -0,0 +1,30 @@ +name: flutter_order_details +description: "A Flutter module for order details." +version: 1.0.0 + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 7.0.0 + +flutter: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic diff --git a/packages/flutter_product_page/.gitignore b/packages/flutter_product_page/.gitignore new file mode 100644 index 0000000..a81f4bf --- /dev/null +++ b/packages/flutter_product_page/.gitignore @@ -0,0 +1,56 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ +.metadata + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# env +*dotenv + +android/ +ios/ +linux/ +macos/ +web/ +windows/ \ No newline at end of file diff --git a/packages/flutter_product_page/README.md b/packages/flutter_product_page/README.md new file mode 100644 index 0000000..fd0ed4d --- /dev/null +++ b/packages/flutter_product_page/README.md @@ -0,0 +1,47 @@ +# flutter_product_page + +This component allows you to easily create and manage the products for any shop. Easily highlight a specific product +and automatically see your products categorized. This package allows users to gather more information about a product, +add it to a custom implementable shopping cart and even navigate to your own shopping cart. + +This component is very customizable, it allows you to adjust basically everything while providing clean defaults. + +## Features + +* Easily navigate between different shops, +* Show users a highlighted product, +* Integrate with your own shopping cart, +* Automatically categorized products, powered by the `flutter_nested_categories` package that you have full control over, even in this component. + +## Usage + +First, you must implement your own `Shop` and `Product` classes. Your shop class must extend from the `ProductPageShop` class provided by this module. Your `Product` class should extend from the `Product` class provided by this module. + +Next, you can create a `ProductPage` or a `ProductPageScreen`. The choice for the former is when you do not want to create a new Scaffold and the latter for when you want to create a new Scaffold. + +To show the page, you must configure what you want to show. Both the `ProductPage` and the `ProductPageScreen` take a parameter that is a `ProductPageConfiguration`. This allows you for a lot of customizability, including what shops there are and what products to show. + +For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_product_page/tree/main/example). + +Or, you could run the example yourself: +``` +git clone https://github.com/Iconica-Development/flutter_product_page.git + +cd flutter_product_page + +cd example + +flutter run +``` + +## Issues + +Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_product_page) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). + +## Want to contribute + +If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_product_page/pulls). + +## Author + +This flutter_product_page for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at diff --git a/packages/flutter_product_page/analysis_options.yaml b/packages/flutter_product_page/analysis_options.yaml new file mode 100644 index 0000000..0736605 --- /dev/null +++ b/packages/flutter_product_page/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_iconica_analysis/components_options.yaml + +# Possible to overwrite the rules from the package + +analyzer: + exclude: + +linter: + rules: diff --git a/packages/flutter_product_page/lib/flutter_product_page.dart b/packages/flutter_product_page/lib/flutter_product_page.dart new file mode 100644 index 0000000..2814df5 --- /dev/null +++ b/packages/flutter_product_page/lib/flutter_product_page.dart @@ -0,0 +1,13 @@ +/// Module for creating a product page with a list of products and a +/// detailed view of each product. +library flutter_product_page; + +export "src/configuration/product_page_category_styling_configuration.dart"; +export "src/configuration/product_page_configuration.dart"; +export "src/configuration/product_page_content.dart"; +export "src/configuration/product_page_localization.dart"; +export "src/configuration/product_page_shop_selector_style.dart"; +export "src/models/product.dart"; +export "src/models/product_page_shop.dart"; +export "src/ui/product_page.dart"; +export "src/ui/product_page_screen.dart"; diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_category_styling_configuration.dart b/packages/flutter_product_page/lib/src/configuration/product_page_category_styling_configuration.dart new file mode 100644 index 0000000..f554222 --- /dev/null +++ b/packages/flutter_product_page/lib/src/configuration/product_page_category_styling_configuration.dart @@ -0,0 +1,54 @@ +import "package:flutter/material.dart"; +import "package:flutter_nested_categories/flutter_nested_categories.dart" + show CategoryHeaderStyling; + +/// Configuration for the styling of the category list on the product page. +/// This configuration allows to customize the title, header styling and +/// the collapsible behavior of the categories. +class ProductPageCategoryStylingConfiguration { + /// Constructor to create a new instance of + /// [ProductPageCategoryStylingConfiguration]. + const ProductPageCategoryStylingConfiguration({ + this.headerStyling, + this.headerCentered = false, + this.customTitle, + this.title, + this.titleStyle, + this.titleCentered = false, + this.isCategoryCollapsible = true, + }); + + /// Optional title for the category list. This will be displayed at the + /// top of the list. + final String? title; + + /// Optional custom title widget for the category list. This will be + /// displayed at the top of the list. If set, the text title will be + /// ignored. + final Widget? customTitle; + + /// Optional title style for the title of the category list. This will + /// be applied to the title of the category list. If not set, the default + /// text style will be used. + final TextStyle? titleStyle; + + /// Configure if the title should be centered. + /// + /// Default is false. + final bool titleCentered; + + /// Optional header styling for the categories. This will be applied to + /// the name of the categories. If not set, the default text style will + /// be used. + final CategoryHeaderStyling? headerStyling; + + /// Configure if the category header should be centered. + /// + /// Default is false. + final bool headerCentered; + + /// Configure if the category should be collapsible. + /// + /// Default is true. + final bool isCategoryCollapsible; +} diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart b/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart new file mode 100644 index 0000000..a08c3d0 --- /dev/null +++ b/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart @@ -0,0 +1,202 @@ +import "package:flutter/material.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_product_page/src/ui/widgets/product_item_popup.dart"; + +/// Configuration for the product page. +class ProductPageConfiguration { + /// Constructor for the product page configuration. + ProductPageConfiguration({ + required this.shops, + // + required this.getProducts, + // + required this.onAddToCart, + required this.onNavigateToShoppingCart, + this.navigateToShoppingCartBuilder, + // + this.initialShopId, + // + this.productBuilder, + // + this.onShopSelectionChange, + this.getProductsInShoppingCart, + // + this.localizations = const ProductPageLocalization(), + // + this.shopSelectorStyle = ShopSelectorStyle.spacedWrap, + this.categoryStylingConfiguration = + const ProductPageCategoryStylingConfiguration(), + // + this.pagePadding = const EdgeInsets.all(4), + // + this.appBar, + this.bottomNavigationBar, + // + Function( + BuildContext context, + ProductPageProduct product, + )? onProductDetail, + String Function( + ProductPageProduct product, + )? getDiscountDescription, + Widget Function( + BuildContext context, + ProductPageProduct product, + )? productPopupBuilder, + Widget Function( + BuildContext context, + )? noContentBuilder, + Widget Function( + BuildContext context, + Object? error, + StackTrace? stackTrace, + )? errorBuilder, + }) { + _productPopupBuilder = productPopupBuilder; + _productPopupBuilder ??= + (BuildContext context, ProductPageProduct product) => ProductItemPopup( + product: product, + configuration: this, + ); + + _onProductDetail = onProductDetail; + _onProductDetail ??= + (BuildContext context, ProductPageProduct product) async { + var theme = Theme.of(context); + + await showModalBottomSheet( + context: context, + backgroundColor: theme.colorScheme.surface, + builder: (context) => _productPopupBuilder!( + context, + product, + ), + ); + }; + + _noContentBuilder = noContentBuilder; + _noContentBuilder ??= (BuildContext context) { + var theme = Theme.of(context); + return Center( + child: Text( + "No content", + style: theme.textTheme.titleLarge, + ), + ); + }; + + _errorBuilder = errorBuilder; + _errorBuilder ??= + (BuildContext context, Object? error, StackTrace? stackTrace) { + var theme = Theme.of(context); + return Center( + child: Text( + "Error: $error", + style: theme.textTheme.titleLarge, + ), + ); + }; + + _getDiscountDescription = getDiscountDescription; + _getDiscountDescription ??= + (ProductPageProduct product) => "${product.name} is on sale!"; + } + + /// The shop that is initially selected. + final String? initialShopId; + + /// A list of all the shops that the user must be able to navigate from. + final Future> shops; + + /// A function that returns all the products that belong to a certain shop. + /// The function must return a [ProductPageContent] object. + final Future Function(ProductPageShop shop) getProducts; + + /// The localizations for the product page. + final ProductPageLocalization localizations; + + /// Builder for the product item. These items will be displayed in the list + /// for each product in their seperated category. This builder should only + /// build the widget for one specific product. This builder has a default + /// in-case the developer does not override it. + Widget Function(BuildContext context, ProductPageProduct product)? + productBuilder; + + late Widget Function(BuildContext context, ProductPageProduct product)? + _productPopupBuilder; + + /// The builder for the product popup. This popup will be displayed when the + /// user clicks on a product. This builder should only build the widget that + /// displays the content of one specific product. + /// This builder has a default in-case the developer + Widget Function(BuildContext context, ProductPageProduct product) + get productPopupBuilder => _productPopupBuilder!; + + late Function(BuildContext context, ProductPageProduct product)? + _onProductDetail; + + /// This function handles the creation of the product detail popup. This + /// function has a default in-case the developer does not override it. + /// The default intraction is a popup, but this can be overriden. + Function(BuildContext context, ProductPageProduct product) + get onProductDetail => _onProductDetail!; + + late Widget Function(BuildContext context)? _noContentBuilder; + + /// The no content builder is used when a shop has no products. This builder + /// has a default in-case the developer does not override it. + Function(BuildContext context)? get noContentBuilder => _noContentBuilder; + + /// The builder for the shopping cart. This builder should return a widget + /// that navigates to the shopping cart overview page. + Widget Function(BuildContext context)? navigateToShoppingCartBuilder; + + late Widget Function( + BuildContext context, + Object? error, + StackTrace? stackTrace, + )? _errorBuilder; + + /// The error builder is used when an error occurs. This builder has a default + /// in-case the developer does not override it. + Widget Function(BuildContext context, Object? error, StackTrace? stackTrace)? + get errorBuilder => _errorBuilder; + + late String Function(ProductPageProduct product)? _getDiscountDescription; + + /// The function that returns the description of the discount for a product. + /// This allows you to translate and give custom messages for each product. + String Function(ProductPageProduct product)? get getDiscountDescription => + _getDiscountDescription!; + + /// This function must be implemented by the developer and should handle the + /// adding of a product to the cart. + Function(ProductPageProduct product) onAddToCart; + + /// This function gets executed when the user changes the shop selection. + /// This function always fires upon first load with the initial shop as well. + final Function(ProductPageShop shop)? onShopSelectionChange; + + /// This function must be implemented by the developer and should handle the + /// navigation to the shopping cart overview page. + final int Function()? getProductsInShoppingCart; + + /// This function must be implemented by the developer and should handle the + /// navigation to the shopping cart overview page. + final Function() onNavigateToShoppingCart; + + /// The style of the shop selector. + final ShopSelectorStyle shopSelectorStyle; + + /// The styling configuration for the category list. + final ProductPageCategoryStylingConfiguration categoryStylingConfiguration; + + /// The padding for the page. + final EdgeInsets pagePadding; + + /// Optional app bar that you can pass to the product page screen. + final Widget? bottomNavigationBar; + + /// Optional app bar that you can pass to the order detail screen. + final PreferredSizeWidget? appBar; +} diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_content.dart b/packages/flutter_product_page/lib/src/configuration/product_page_content.dart new file mode 100644 index 0000000..ec172a4 --- /dev/null +++ b/packages/flutter_product_page/lib/src/configuration/product_page_content.dart @@ -0,0 +1,16 @@ +import "package:flutter_product_page/flutter_product_page.dart"; + +/// Return type that contains the products and an optional discounted product. +class ProductPageContent { + /// Default constructor for this class. + const ProductPageContent({ + required this.products, + this.discountedProduct, + }); + + /// List of products that belong to the shop. + final List products; + + /// Optional highlighted discounted product to display. + final ProductPageProduct? discountedProduct; +} diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_localization.dart b/packages/flutter_product_page/lib/src/configuration/product_page_localization.dart new file mode 100644 index 0000000..47990f6 --- /dev/null +++ b/packages/flutter_product_page/lib/src/configuration/product_page_localization.dart @@ -0,0 +1,22 @@ +/// Localization for the product page +class ProductPageLocalization { + /// Default constructor + const ProductPageLocalization({ + this.navigateToShoppingCart = "To shopping cart", + this.discountTitle = "Discount", + this.failedToLoadImageExplenation = "Failed to load image", + this.close = "Close", + }); + + /// Message to navigate to the shopping cart + final String navigateToShoppingCart; + + /// Title for the discount + final String discountTitle; + + /// Explenation when the image failed to load + final String failedToLoadImageExplenation; + + /// Close button for the product page + final String close; +} diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_shop_selector_style.dart b/packages/flutter_product_page/lib/src/configuration/product_page_shop_selector_style.dart new file mode 100644 index 0000000..e37c07a --- /dev/null +++ b/packages/flutter_product_page/lib/src/configuration/product_page_shop_selector_style.dart @@ -0,0 +1,8 @@ +/// Style for the shop selector in the product page. +enum ShopSelectorStyle { + /// Shops are displayed in a row. + row, + + /// Shops are displayed in a wrap. + spacedWrap, +} diff --git a/packages/flutter_product_page/lib/src/models/product.dart b/packages/flutter_product_page/lib/src/models/product.dart new file mode 100644 index 0000000..4e09fdb --- /dev/null +++ b/packages/flutter_product_page/lib/src/models/product.dart @@ -0,0 +1,26 @@ +/// The product page shop class contains all the required information +/// +/// This is a mixin class because another package will implement it, and the +/// 'MyProduct' class might have to extend another class as well. +mixin ProductPageProduct { + /// The unique identifier for the product. + String get id; + + /// The name of the product. + String get name; + + /// The image URL of the product. + String get imageUrl; + + /// The category of the product. + String get category; + + /// The price of the product. + double get price; + + /// Whether the product has a discount or not. + bool get hasDiscount; + + /// The discounted price of the product. Only used if [hasDiscount] is true. + double? get discountPrice; +} diff --git a/packages/flutter_product_page/lib/src/models/product_page_shop.dart b/packages/flutter_product_page/lib/src/models/product_page_shop.dart new file mode 100644 index 0000000..a4d0aa9 --- /dev/null +++ b/packages/flutter_product_page/lib/src/models/product_page_shop.dart @@ -0,0 +1,18 @@ +/// The product page shop class contains all the required information +/// that needs to be known about a certain shop. +/// +/// In your own implemententation, you must extend from this class so you can +/// add more fields to this class to suit your needs. +class ProductPageShop { + /// The default constructor for this class. + const ProductPageShop({ + required this.id, + required this.name, + }); + + /// The unique identifier for the shop. + final String id; + + /// The name of the shop. + final String name; +} diff --git a/packages/flutter_product_page/lib/src/services/category_service.dart b/packages/flutter_product_page/lib/src/services/category_service.dart new file mode 100644 index 0000000..76541be --- /dev/null +++ b/packages/flutter_product_page/lib/src/services/category_service.dart @@ -0,0 +1,73 @@ +import "package:flutter/material.dart"; +import "package:flutter_nested_categories/flutter_nested_categories.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_product_page/src/services/shopping_cart_notifier.dart"; +import "package:flutter_product_page/src/ui/components/product_item.dart"; + +/// A function that is called when a product is added to the cart. +ProductPageProduct onAddToCartWrapper( + ProductPageConfiguration configuration, + ShoppingCartNotifier shoppingCartNotifier, + ProductPageProduct product, +) { + shoppingCartNotifier.productsChanged(); + + configuration.onAddToCart(product); + + return product; +} + +/// Generates a [CategoryList] from a list of [Product]s and a +/// [ProductPageConfiguration]. +CategoryList getCategoryList( + BuildContext context, + ProductPageConfiguration configuration, + ShoppingCartNotifier shoppingCartNotifier, + List products, +) { + var categorizedProducts = >{}; + for (var product in products) { + if (!categorizedProducts.containsKey(product.category)) { + categorizedProducts[product.category] = []; + } + categorizedProducts[product.category]?.add(product); + } + + // Create Category instances + var categories = []; + categorizedProducts.forEach((categoryName, productList) { + var productWidgets = productList + .map( + (product) => configuration.productBuilder != null + ? configuration.productBuilder!(context, product) + : ProductItem( + product: product, + onProductDetail: configuration.onProductDetail, + onAddToCart: (ProductPageProduct product) => + onAddToCartWrapper( + configuration, + shoppingCartNotifier, + product, + ), + localizations: configuration.localizations, + ), + ) + .toList(); + var category = Category( + name: categoryName, + content: productWidgets, + ); + categories.add(category); + }); + + return CategoryList( + title: configuration.categoryStylingConfiguration.title, + titleStyle: configuration.categoryStylingConfiguration.titleStyle, + customTitle: configuration.categoryStylingConfiguration.customTitle, + headerCentered: configuration.categoryStylingConfiguration.headerCentered, + headerStyling: configuration.categoryStylingConfiguration.headerStyling, + isCategoryCollapsible: + configuration.categoryStylingConfiguration.isCategoryCollapsible, + content: categories, + ); +} diff --git a/packages/flutter_product_page/lib/src/services/selected_shop_service.dart b/packages/flutter_product_page/lib/src/services/selected_shop_service.dart new file mode 100644 index 0000000..502eaa5 --- /dev/null +++ b/packages/flutter_product_page/lib/src/services/selected_shop_service.dart @@ -0,0 +1,21 @@ +import "package:flutter/material.dart"; +import "package:flutter_product_page/src/models/product_page_shop.dart"; + +/// A service that provides the currently selected shop. +class SelectedShopService extends ChangeNotifier { + /// Creates a [SelectedShopService]. + SelectedShopService(); + + ProductPageShop? _selectedShop; + + /// Updates the selected shop. + void selectShop(ProductPageShop shop) { + if (_selectedShop == shop) return; + + _selectedShop = shop; + notifyListeners(); + } + + /// The currently selected shop. + ProductPageShop? get selectedShop => _selectedShop; +} diff --git a/packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart b/packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart new file mode 100644 index 0000000..d02251f --- /dev/null +++ b/packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart @@ -0,0 +1,10 @@ +import "package:flutter/material.dart"; + +/// Class that notifies listeners when the products in the shopping cart have +/// changed. +class ShoppingCartNotifier extends ChangeNotifier { + /// Notifies listeners that the products in the shopping cart have changed. + void productsChanged() { + notifyListeners(); + } +} diff --git a/packages/flutter_product_page/lib/src/ui/components/product_item.dart b/packages/flutter_product_page/lib/src/ui/components/product_item.dart new file mode 100644 index 0000000..05ed65d --- /dev/null +++ b/packages/flutter_product_page/lib/src/ui/components/product_item.dart @@ -0,0 +1,195 @@ +import "package:cached_network_image/cached_network_image.dart"; +import "package:flutter/material.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:skeletonizer/skeletonizer.dart"; + +/// Product item widget. +class ProductItem extends StatelessWidget { + /// Constructor for the product item widget. + const ProductItem({ + required this.product, + required this.onProductDetail, + required this.onAddToCart, + required this.localizations, + super.key, + }); + + /// Product to display. + final ProductPageProduct product; + + /// Function to call when the product detail is requested. + final Function(BuildContext context, ProductPageProduct selectedProduct) + onProductDetail; + + /// Function to call when the product is added to the cart. + final Function(ProductPageProduct selectedProduct) onAddToCart; + + /// Localizations for the product page. + final ProductPageLocalization localizations; + + /// Size of the product image. + static const double imageSize = 44; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var loadingImageSkeleton = const Skeletonizer.zone( + child: SizedBox(width: imageSize, height: imageSize, child: Bone.icon()), + ); + + var productIcon = ClipRRect( + borderRadius: BorderRadius.circular(4), + child: CachedNetworkImage( + imageUrl: product.imageUrl, + width: imageSize, + height: imageSize, + fit: BoxFit.cover, + placeholder: (context, url) => loadingImageSkeleton, + errorWidget: (context, url, error) => Tooltip( + message: localizations.failedToLoadImageExplenation, + child: Container( + width: 48, + height: 48, + alignment: Alignment.center, + child: const Icon( + Icons.error_outline_sharp, + color: Colors.red, + ), + ), + ), + ), + ); + + var productName = Padding( + padding: const EdgeInsets.only(left: 8), + child: Container( + constraints: const BoxConstraints(maxWidth: 150), + child: Text( + product.name, + style: theme.textTheme.titleMedium, + ), + ), + ); + + var productInformationIcon = Padding( + padding: const EdgeInsets.only(left: 4), + child: IconButton( + onPressed: () => onProductDetail(context, product), + icon: const Icon(Icons.info_outline), + ), + ); + + var productInteraction = Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _PriceLabel( + price: product.price, + discountPrice: (product.hasDiscount && product.discountPrice != null) + ? product.discountPrice + : null, + ), + _AddToCardButton( + product: product, + onAddToCart: onAddToCart, + ), + ], + ); + + return Padding( + padding: const EdgeInsets.only(top: 12, bottom: 8), + child: Row( + children: [ + productIcon, + productName, + productInformationIcon, + const Spacer(), + productInteraction, + ], + ), + ); + } +} + +class _PriceLabel extends StatelessWidget { + const _PriceLabel({ + required this.price, + required this.discountPrice, + }); + + final double price; + final double? discountPrice; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + if (discountPrice == null) + return Text( + price.toStringAsFixed(2), + style: theme.textTheme.bodyMedium, + ); + else + return Row( + children: [ + Text( + price.toStringAsFixed(2), + style: theme.textTheme.bodySmall?.copyWith( + fontSize: 10, + color: theme.colorScheme.primary, + decoration: TextDecoration.lineThrough, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + discountPrice!.toStringAsFixed(2), + style: theme.textTheme.bodyMedium, + ), + ), + ], + ); + } +} + +class _AddToCardButton extends StatelessWidget { + const _AddToCardButton({ + required this.product, + required this.onAddToCart, + }); + + final ProductPageProduct product; + final Function(ProductPageProduct product) onAddToCart; + + static const double boxSize = 29; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return SizedBox( + width: boxSize, + height: boxSize, + child: Center( + child: IconButton( + padding: EdgeInsets.zero, + icon: Icon( + Icons.add, + color: theme.primaryColor, + size: 20, + ), + onPressed: () => onAddToCart(product), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + theme.colorScheme.secondary, + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ), + ); + } +} diff --git a/packages/flutter_product_page/lib/src/ui/components/shop_selector.dart b/packages/flutter_product_page/lib/src/ui/components/shop_selector.dart new file mode 100644 index 0000000..c9b3b76 --- /dev/null +++ b/packages/flutter_product_page/lib/src/ui/components/shop_selector.dart @@ -0,0 +1,63 @@ +import "package:flutter/material.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_product_page/src/services/selected_shop_service.dart"; +import "package:flutter_product_page/src/ui/widgets/horizontal_list_items.dart"; +import "package:flutter_product_page/src/ui/widgets/spaced_wrap.dart"; + +/// Shop selector widget that displays a list to navigate between shops. +class ShopSelector extends StatelessWidget { + /// Constructor for the shop selector. + const ShopSelector({ + required this.configuration, + required this.selectedShopService, + required this.shops, + required this.onTap, + this.paddingBetweenButtons = 4, + this.paddingOnButtons = 8, + super.key, + }); + + /// Configuration for the product page. + final ProductPageConfiguration configuration; + + /// Service for the selected shop. + final SelectedShopService selectedShopService; + + /// List of shops. + final List shops; + + /// Callback when a shop is tapped. + final Function(ProductPageShop shop) onTap; + + /// Padding between the buttons. + final double paddingBetweenButtons; + + /// Padding on the buttons. + final double paddingOnButtons; + + @override + Widget build(BuildContext context) { + if (shops.length == 1) { + return const SizedBox.shrink(); + } + + if (configuration.shopSelectorStyle == ShopSelectorStyle.spacedWrap) { + return SpacedWrap( + shops: shops, + selectedItem: selectedShopService.selectedShop!.id, + onTap: onTap, + width: MediaQuery.of(context).size.width - (16 * 2), + paddingBetweenButtons: paddingBetweenButtons, + paddingOnButtons: paddingOnButtons, + ); + } + + return HorizontalListItems( + shops: shops, + selectedItem: selectedShopService.selectedShop!.id, + onTap: onTap, + paddingBetweenButtons: paddingBetweenButtons, + paddingOnButtons: paddingOnButtons, + ); + } +} diff --git a/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart b/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart new file mode 100644 index 0000000..80da812 --- /dev/null +++ b/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart @@ -0,0 +1,127 @@ +import "package:cached_network_image/cached_network_image.dart"; +import "package:flutter/material.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; + +/// A widget that displays a weekly discount. +class WeeklyDiscount extends StatelessWidget { + /// Creates a weekly discount. + const WeeklyDiscount({ + required this.configuration, + required this.product, + super.key, + }); + + /// Configuration for the product page. + final ProductPageConfiguration configuration; + + /// The product for which the discount is displayed. + final ProductPageProduct product; + + /// The top padding of the widget. + static const double topPadding = 32.0; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var bottomText = Padding( + padding: const EdgeInsets.all(20.0), + child: Text( + configuration.getDiscountDescription!(product), + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.primary, + ), + textAlign: TextAlign.left, + ), + ); + + var loadingImage = const Padding( + padding: EdgeInsets.all(32.0), + child: Center( + child: CircularProgressIndicator.adaptive(), + ), + ); + + var errorImage = Padding( + padding: const EdgeInsets.all(32.0), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline_rounded, + color: Colors.red, + ), + Text(configuration.localizations.failedToLoadImageExplenation), + ], + ), + ), + ); + + var image = Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: AspectRatio( + aspectRatio: 1, + child: CachedNetworkImage( + imageUrl: product.imageUrl, + width: double.infinity, + fit: BoxFit.cover, + placeholder: (context, url) => loadingImage, + errorWidget: (context, url, error) => errorImage, + ), + ), + ); + + var topText = DecoratedBox( + decoration: BoxDecoration( + color: theme.primaryColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + ), + child: SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Text( + configuration.localizations.discountTitle.toUpperCase(), + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onPrimary, + ), + textAlign: TextAlign.left, + ), + ), + ), + ); + + var boxDecoration = BoxDecoration( + border: Border.all( + color: theme.primaryColor, + width: 1.0, + ), + borderRadius: BorderRadius.circular(4.0), + ); + + return Padding( + padding: const EdgeInsets.only(top: topPadding), + child: DecoratedBox( + decoration: boxDecoration, + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + topText, + image, + bottomText, + ], + ), + ), + ), + ); + } +} diff --git a/packages/flutter_product_page/lib/src/ui/product_page.dart b/packages/flutter_product_page/lib/src/ui/product_page.dart new file mode 100644 index 0000000..5c99990 --- /dev/null +++ b/packages/flutter_product_page/lib/src/ui/product_page.dart @@ -0,0 +1,288 @@ +import "package:flutter/material.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_product_page/src/services/category_service.dart"; +import "package:flutter_product_page/src/services/selected_shop_service.dart"; +import "package:flutter_product_page/src/services/shopping_cart_notifier.dart"; +import "package:flutter_product_page/src/ui/components/shop_selector.dart"; +import "package:flutter_product_page/src/ui/components/weekly_discount.dart"; + +/// A page that displays products. +class ProductPage extends StatelessWidget { + /// Constructor for the product page. + ProductPage({ + required this.configuration, + this.initialBuildShopId, + super.key, + }); + + /// Configuration for the product page. + final ProductPageConfiguration configuration; + + /// An optional initial shop ID to select. This overrides the initialShopId + /// from the configuration. + final String? initialBuildShopId; + + late final SelectedShopService _selectedShopService = SelectedShopService(); + + late final ShoppingCartNotifier _shoppingCartNotifier = + ShoppingCartNotifier(); + + @override + Widget build(BuildContext context) => Padding( + padding: configuration.pagePadding, + child: FutureBuilder( + future: configuration.shops, + builder: (BuildContext context, AsyncSnapshot data) { + if (data.connectionState == ConnectionState.waiting) { + return const Align( + alignment: Alignment.center, + child: CircularProgressIndicator.adaptive(), + ); + } + + if (data.hasError) { + return configuration.errorBuilder!( + context, + data.error, + data.stackTrace, + ); + } + + List? shops = data.data; + + if (shops == null || shops.isEmpty) { + return configuration.errorBuilder!(context, null, null); + } + + if (initialBuildShopId != null) { + ProductPageShop? initialShop; + + for (var shop in shops) { + if (shop.id == initialBuildShopId) { + initialShop = shop; + break; + } + } + + _selectedShopService.selectShop(initialShop ?? shops.first); + } else if (configuration.initialShopId != null) { + ProductPageShop? initialShop; + + for (var shop in shops) { + if (shop.id == configuration.initialShopId) { + initialShop = shop; + break; + } + } + + _selectedShopService.selectShop(initialShop ?? shops.first); + } else { + _selectedShopService.selectShop(shops.first); + } + + return ListenableBuilder( + listenable: _selectedShopService, + builder: (BuildContext context, Widget? _) { + configuration.onShopSelectionChange?.call( + _selectedShopService.selectedShop!, + ); + return _ProductPage( + configuration: configuration, + selectedShopService: _selectedShopService, + shoppingCartNotifier: _shoppingCartNotifier, + shops: shops, + ); + }, + ); + }, + ), + ); +} + +class _ProductPage extends StatelessWidget { + const _ProductPage({ + required this.configuration, + required this.selectedShopService, + required this.shoppingCartNotifier, + required this.shops, + }); + + final ProductPageConfiguration configuration; + final SelectedShopService selectedShopService; + final ShoppingCartNotifier shoppingCartNotifier; + + final List shops; + + void _onTapChangeShop(ProductPageShop shop) { + selectedShopService.selectShop(shop); + } + + @override + Widget build(BuildContext context) { + var pageContent = SingleChildScrollView( + child: Column( + children: [ + ShopSelector( + configuration: configuration, + selectedShopService: selectedShopService, + shops: shops, + onTap: _onTapChangeShop, + ), + _ShopContents( + configuration: configuration, + selectedShopService: selectedShopService, + shoppingCartNotifier: shoppingCartNotifier, + ), + ], + ), + ); + + return Stack( + children: [ + pageContent, + Align( + alignment: Alignment.bottomCenter, + child: configuration.navigateToShoppingCartBuilder != null + ? configuration.navigateToShoppingCartBuilder!(context) + : _NavigateToShoppingCartButton( + configuration: configuration, + shoppingCartNotifier: shoppingCartNotifier, + ), + ), + ], + ); + } +} + +class _NavigateToShoppingCartButton extends StatelessWidget { + const _NavigateToShoppingCartButton({ + required this.configuration, + required this.shoppingCartNotifier, + }); + + final ProductPageConfiguration configuration; + final ShoppingCartNotifier shoppingCartNotifier; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + String getProductsInShoppingCartLabel() { + var fun = configuration.getProductsInShoppingCart; + + if (fun == null) { + return ""; + } + + return "(${fun()})"; + } + + return FilledButton( + onPressed: configuration.onNavigateToShoppingCart, + style: theme.filledButtonTheme.style?.copyWith( + backgroundColor: WidgetStateProperty.all( + theme.colorScheme.primary, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: ListenableBuilder( + listenable: shoppingCartNotifier, + builder: (BuildContext context, Widget? _) => Text( + """${configuration.localizations.navigateToShoppingCart.toUpperCase()} ${getProductsInShoppingCartLabel()}""", + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onPrimary, + ), + ), + ), + ), + ); + } +} + +class _ShopContents extends StatelessWidget { + const _ShopContents({ + required this.configuration, + required this.selectedShopService, + required this.shoppingCartNotifier, + }); + + final ProductPageConfiguration configuration; + final SelectedShopService selectedShopService; + final ShoppingCartNotifier shoppingCartNotifier; + + @override + Widget build(BuildContext context) => Padding( + padding: EdgeInsets.symmetric( + horizontal: configuration.pagePadding.horizontal, + ), + child: FutureBuilder( + // ignore: discarded_futures + future: configuration.getProducts( + selectedShopService.selectedShop!, + ), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Align( + alignment: Alignment.center, + child: CircularProgressIndicator.adaptive(), + ); + } + + if (snapshot.hasError) { + return configuration.errorBuilder!( + context, + snapshot.error, + snapshot.stackTrace, + ); + } + + var productPageContent = snapshot.data; + + if (productPageContent == null || + productPageContent.products.isEmpty) { + return configuration.noContentBuilder!(context); + } + + var productList = Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), + child: Column( + children: [ + // Products + getCategoryList( + context, + configuration, + shoppingCartNotifier, + productPageContent.products, + ), + + // Bottom padding so the last product is not cut off + // by the to shopping cart button. + const SizedBox(height: 48), + ], + ), + ); + + return Column( + children: [ + // Discounted product + if (productPageContent.discountedProduct != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: WeeklyDiscount( + configuration: configuration, + product: productPageContent.discountedProduct!, + ), + ), + ], + + productList, + ], + ); + }, + ), + ); +} diff --git a/packages/flutter_product_page/lib/src/ui/product_page_screen.dart b/packages/flutter_product_page/lib/src/ui/product_page_screen.dart new file mode 100644 index 0000000..959626b --- /dev/null +++ b/packages/flutter_product_page/lib/src/ui/product_page_screen.dart @@ -0,0 +1,36 @@ +import "package:flutter/material.dart"; +import "package:flutter_product_page/src/configuration/product_page_configuration.dart"; +import "package:flutter_product_page/src/ui/product_page.dart"; + +/// A screen that displays a product page. This screen contains a Scaffold, +/// in which the body is a SafeArea that contains a ProductPage widget. +/// +/// If you do not wish to create a Scaffold you can use the +/// [ProductPage] widget directly. +class ProductPageScreen extends StatelessWidget { + /// Constructor for the product page screen. + const ProductPageScreen({ + required this.configuration, + this.initialBuildShopId, + super.key, + }); + + /// Configuration for the product page. + final ProductPageConfiguration configuration; + + /// An optional initial shop ID to select. This overrides the initialShopId + /// from the configuration. + final String? initialBuildShopId; + + @override + Widget build(BuildContext context) => Scaffold( + body: SafeArea( + child: ProductPage( + configuration: configuration, + initialBuildShopId: initialBuildShopId, + ), + ), + appBar: configuration.appBar, + bottomNavigationBar: configuration.bottomNavigationBar, + ); +} diff --git a/packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart b/packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart new file mode 100644 index 0000000..8079723 --- /dev/null +++ b/packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart @@ -0,0 +1,73 @@ +import "package:flutter/material.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; + +/// Horizontal list of items. +class HorizontalListItems extends StatelessWidget { + /// Constructor for the horizontal list of items. + const HorizontalListItems({ + required this.shops, + required this.selectedItem, + required this.onTap, + this.paddingBetweenButtons = 2.0, + this.paddingOnButtons = 4, + super.key, + }); + + /// List of items. + final List shops; + + /// Selected item. + final String selectedItem; + + /// Padding between the buttons. + final double paddingBetweenButtons; + + /// Padding on the buttons. + final double paddingOnButtons; + + /// Callback when an item is tapped. + final Function(ProductPageShop shop) onTap; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: shops + .map( + (shop) => Padding( + padding: EdgeInsets.only(right: paddingBetweenButtons), + child: InkWell( + onTap: () => onTap(shop), + child: Container( + decoration: BoxDecoration( + color: shop.id == selectedItem + ? theme.colorScheme.primary + : theme.colorScheme.secondary, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: theme.colorScheme.primary, + width: 1, + ), + ), + padding: EdgeInsets.all(paddingOnButtons), + child: Text( + shop.name, + style: shop.id == selectedItem + ? theme.textTheme.bodyMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ) + : theme.textTheme.bodyMedium, + ), + ), + ), + ), + ) + .toList(), + ), + ); + } +} diff --git a/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart b/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart new file mode 100644 index 0000000..5186ef5 --- /dev/null +++ b/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart @@ -0,0 +1,69 @@ +import "package:flutter/material.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; + +/// A popup that displays the product item. +class ProductItemPopup extends StatelessWidget { + /// Constructor for the product item popup. + const ProductItemPopup({ + required this.product, + required this.configuration, + super.key, + }); + + /// The product to display. + final ProductPageProduct product; + + /// Configuration for the product page. + final ProductPageConfiguration configuration; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var productDescription = Padding( + padding: const EdgeInsets.fromLTRB(44, 32, 44, 20), + child: Text( + product.name, + textAlign: TextAlign.center, + ), + ); + + var closeButton = Padding( + padding: const EdgeInsets.fromLTRB(80, 0, 80, 32), + child: SizedBox( + width: 254, + child: ElevatedButton( + style: theme.elevatedButtonTheme.style?.copyWith( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + ), + onPressed: () => Navigator.of(context).pop(), + child: Padding( + padding: const EdgeInsets.all(14), + child: Text( + configuration.localizations.close, + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + ), + ), + ), + ); + + return SingleChildScrollView( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + productDescription, + closeButton, + ], + ), + ), + ); + } +} diff --git a/packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart b/packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart new file mode 100644 index 0000000..9bfa8b4 --- /dev/null +++ b/packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart @@ -0,0 +1,150 @@ +import "package:flutter/material.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; + +/// SpacedWrap is a widget that wraps a list of items that are spaced out and +/// fill the available width. +class SpacedWrap extends StatelessWidget { + /// Creates a [SpacedWrap]. + const SpacedWrap({ + required this.shops, + required this.onTap, + required this.width, + this.paddingBetweenButtons = 2.0, + this.paddingOnButtons = 4.0, + this.selectedItem = "", + super.key, + }); + + /// List of items. + final List shops; + + /// Selected item. + final String selectedItem; + + /// Width of the widget. + final double width; + + /// Padding between the buttons. + final double paddingBetweenButtons; + + /// Padding on the buttons. + final double paddingOnButtons; + + /// Callback when an item is tapped. + final Function(ProductPageShop shop) onTap; + + Row _buildRow( + BuildContext context, + List currentRow, + double availableRowLength, + ) { + var theme = Theme.of(context); + + var row = []; + var extraButtonPadding = availableRowLength / currentRow.length / 2; + + for (var i = 0, len = currentRow.length; i < len; i++) { + var shop = shops[currentRow[i]]; + row.add( + Padding( + padding: EdgeInsets.only(top: paddingBetweenButtons), + child: InkWell( + onTap: () => onTap(shop), + child: Container( + decoration: BoxDecoration( + color: shop.id == selectedItem + ? theme.colorScheme.primary + : theme.colorScheme.secondary, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: theme.colorScheme.primary, + width: 1, + ), + ), + padding: EdgeInsets.symmetric( + horizontal: paddingOnButtons + extraButtonPadding, + vertical: paddingOnButtons, + ), + child: Text( + shop.name, + style: shop.id == selectedItem + ? theme.textTheme.bodyMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ) + : theme.textTheme.bodyMedium, + ), + ), + ), + ), + ); + if (shops.last != shop) { + row.add(const Spacer()); + } + } + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: row, + ); + } + + List _buildButtonRows(BuildContext context) { + var theme = Theme.of(context); + var rows = []; + var currentRow = []; + var availableRowLength = width; + + for (var i = 0; i < shops.length; i++) { + var shop = shops[i]; + + var textPainter = TextPainter( + text: TextSpan( + text: shop.name, + style: shop.id == selectedItem + ? theme.textTheme.bodyMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ) + : theme.textTheme.bodyMedium, + ), + maxLines: 1, + textDirection: TextDirection.ltr, + )..layout(minWidth: 0, maxWidth: double.infinity); + + var buttonWidth = textPainter.width + paddingOnButtons * 2; + + if (availableRowLength - buttonWidth < 0) { + rows.add( + _buildRow( + context, + currentRow, + availableRowLength, + ), + ); + currentRow = []; + availableRowLength = width; + } + + currentRow.add(i); + + availableRowLength -= buttonWidth + paddingBetweenButtons; + } + if (currentRow.isNotEmpty) { + rows.add( + _buildRow( + context, + currentRow, + availableRowLength, + ), + ); + } + return rows; + } + + @override + Widget build(BuildContext context) => Column( + children: _buildButtonRows( + context, + ), + ); +} diff --git a/packages/flutter_product_page/pubspec.yaml b/packages/flutter_product_page/pubspec.yaml new file mode 100644 index 0000000..6ffcb50 --- /dev/null +++ b/packages/flutter_product_page/pubspec.yaml @@ -0,0 +1,38 @@ +name: flutter_product_page +description: "A Flutter module for the product page" +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: '>=3.3.4 <4.0.0' + +dependencies: + flutter: + sdk: flutter + skeletonizer: ^1.1.1 + cached_network_image: ^3.3.1 + flutter_nested_categories: + git: + url: https://github.com/Iconica-Development/flutter_nested_categories + ref: 0.0.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 7.0.0 + +flutter: + uses-material-design: true + + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic diff --git a/packages/flutter_shopping/.gitignore b/packages/flutter_shopping/.gitignore new file mode 100644 index 0000000..6c0ee6d --- /dev/null +++ b/packages/flutter_shopping/.gitignore @@ -0,0 +1,56 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ +.metadata + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# env +*dotenv + +android/ +ios/ +web/ +macos/ +windows/ +linux/ \ No newline at end of file diff --git a/README.md b/packages/flutter_shopping/README.md similarity index 100% rename from README.md rename to packages/flutter_shopping/README.md diff --git a/packages/flutter_shopping/analysis_options.yaml b/packages/flutter_shopping/analysis_options.yaml new file mode 100644 index 0000000..0736605 --- /dev/null +++ b/packages/flutter_shopping/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_iconica_analysis/components_options.yaml + +# Possible to overwrite the rules from the package + +analyzer: + exclude: + +linter: + rules: diff --git a/example/.gitignore b/packages/flutter_shopping/example/.gitignore similarity index 100% rename from example/.gitignore rename to packages/flutter_shopping/example/.gitignore diff --git a/example/README.md b/packages/flutter_shopping/example/README.md similarity index 100% rename from example/README.md rename to packages/flutter_shopping/example/README.md diff --git a/example/analysis_options.yaml b/packages/flutter_shopping/example/analysis_options.yaml similarity index 100% rename from example/analysis_options.yaml rename to packages/flutter_shopping/example/analysis_options.yaml diff --git a/example/lib/main.dart b/packages/flutter_shopping/example/lib/main.dart similarity index 100% rename from example/lib/main.dart rename to packages/flutter_shopping/example/lib/main.dart diff --git a/example/lib/src/configuration/configuration.dart b/packages/flutter_shopping/example/lib/src/configuration/configuration.dart similarity index 97% rename from example/lib/src/configuration/configuration.dart rename to packages/flutter_shopping/example/lib/src/configuration/configuration.dart index 1ce05d8..9184c8b 100644 --- a/example/lib/src/configuration/configuration.dart +++ b/packages/flutter_shopping/example/lib/src/configuration/configuration.dart @@ -3,10 +3,7 @@ import "package:example/src/routes.dart"; import "package:example/src/services/order_service.dart"; import "package:example/src/services/shop_service.dart"; import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; import "package:flutter_shopping/flutter_shopping.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; import "package:go_router/go_router.dart"; // (REQUIRED): Create your own instance of the ProductService. diff --git a/example/lib/src/models/my_product.dart b/packages/flutter_shopping/example/lib/src/models/my_product.dart similarity index 76% rename from example/lib/src/models/my_product.dart rename to packages/flutter_shopping/example/lib/src/models/my_product.dart index d066d31..f5f2f49 100644 --- a/example/lib/src/models/my_product.dart +++ b/packages/flutter_shopping/example/lib/src/models/my_product.dart @@ -1,5 +1,4 @@ -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; class MyProduct extends ShoppingCartProduct with ProductPageProduct { MyProduct({ diff --git a/example/lib/src/models/my_shop.dart b/packages/flutter_shopping/example/lib/src/models/my_shop.dart similarity index 63% rename from example/lib/src/models/my_shop.dart rename to packages/flutter_shopping/example/lib/src/models/my_shop.dart index ddf05b9..9018fcc 100644 --- a/example/lib/src/models/my_shop.dart +++ b/packages/flutter_shopping/example/lib/src/models/my_shop.dart @@ -1,4 +1,4 @@ -import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; class MyShop extends ProductPageShop { const MyShop({ diff --git a/example/lib/src/routes.dart b/packages/flutter_shopping/example/lib/src/routes.dart similarity index 100% rename from example/lib/src/routes.dart rename to packages/flutter_shopping/example/lib/src/routes.dart diff --git a/example/lib/src/services/order_service.dart b/packages/flutter_shopping/example/lib/src/services/order_service.dart similarity index 75% rename from example/lib/src/services/order_service.dart rename to packages/flutter_shopping/example/lib/src/services/order_service.dart index 2dbaa45..fe3ca31 100644 --- a/example/lib/src/services/order_service.dart +++ b/packages/flutter_shopping/example/lib/src/services/order_service.dart @@ -1,5 +1,5 @@ import "package:example/src/models/my_product.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; /// Example implementation of storing an order in a database. void storeOrderInDatabase(List products, OrderResult result) { diff --git a/example/lib/src/services/shop_service.dart b/packages/flutter_shopping/example/lib/src/services/shop_service.dart similarity index 95% rename from example/lib/src/services/shop_service.dart rename to packages/flutter_shopping/example/lib/src/services/shop_service.dart index 414076d..ced2144 100644 --- a/example/lib/src/services/shop_service.dart +++ b/packages/flutter_shopping/example/lib/src/services/shop_service.dart @@ -1,6 +1,6 @@ import "package:example/src/models/my_product.dart"; import "package:example/src/models/my_shop.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; /// This function should have your own implementation. Generally this would /// contain some API call to fetch the list of shops. diff --git a/example/lib/src/ui/homepage.dart b/packages/flutter_shopping/example/lib/src/ui/homepage.dart similarity index 100% rename from example/lib/src/ui/homepage.dart rename to packages/flutter_shopping/example/lib/src/ui/homepage.dart diff --git a/example/lib/src/utils/go_router.dart b/packages/flutter_shopping/example/lib/src/utils/go_router.dart similarity index 100% rename from example/lib/src/utils/go_router.dart rename to packages/flutter_shopping/example/lib/src/utils/go_router.dart diff --git a/example/lib/src/utils/theme.dart b/packages/flutter_shopping/example/lib/src/utils/theme.dart similarity index 100% rename from example/lib/src/utils/theme.dart rename to packages/flutter_shopping/example/lib/src/utils/theme.dart diff --git a/example/pubspec.yaml b/packages/flutter_shopping/example/pubspec.yaml similarity index 67% rename from example/pubspec.yaml rename to packages/flutter_shopping/example/pubspec.yaml index d6ef298..d4a84ed 100644 --- a/example/pubspec.yaml +++ b/packages/flutter_shopping/example/pubspec.yaml @@ -1,7 +1,7 @@ name: example description: Demonstrates how to use the flutter_shopping package." publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.0+1 +version: 1.0.0 environment: sdk: '>=3.3.4 <4.0.0' @@ -19,20 +19,6 @@ dependencies: flutter_shopping: path: ../ - ## Normal Packages - flutter_product_page: - git: - url: https://github.com/Iconica-Development/flutter_product_page - ref: 1.3.3 - flutter_shopping_cart: - git: - url: https://github.com/Iconica-Development/flutter_shopping_cart - ref: 1.1.1 - flutter_order_details: - git: - url: https://github.com/Iconica-Development/flutter_order_details - ref: 1.0.1 - dev_dependencies: flutter_test: sdk: flutter diff --git a/example_amazon/.gitignore b/packages/flutter_shopping/example_amazon/.gitignore similarity index 100% rename from example_amazon/.gitignore rename to packages/flutter_shopping/example_amazon/.gitignore diff --git a/example_amazon/README.md b/packages/flutter_shopping/example_amazon/README.md similarity index 100% rename from example_amazon/README.md rename to packages/flutter_shopping/example_amazon/README.md diff --git a/example_amazon/analysis_options.yaml b/packages/flutter_shopping/example_amazon/analysis_options.yaml similarity index 100% rename from example_amazon/analysis_options.yaml rename to packages/flutter_shopping/example_amazon/analysis_options.yaml diff --git a/example_amazon/lib/main.dart b/packages/flutter_shopping/example_amazon/lib/main.dart similarity index 100% rename from example_amazon/lib/main.dart rename to packages/flutter_shopping/example_amazon/lib/main.dart diff --git a/example_amazon/lib/src/configuration/shopping_configuration.dart b/packages/flutter_shopping/example_amazon/lib/src/configuration/shopping_configuration.dart similarity index 99% rename from example_amazon/lib/src/configuration/shopping_configuration.dart rename to packages/flutter_shopping/example_amazon/lib/src/configuration/shopping_configuration.dart index 3688380..1ab239c 100644 --- a/example_amazon/lib/src/configuration/shopping_configuration.dart +++ b/packages/flutter_shopping/example_amazon/lib/src/configuration/shopping_configuration.dart @@ -2,9 +2,7 @@ import "package:amazon/src/models/my_product.dart"; import "package:amazon/src/routes.dart"; import "package:amazon/src/services/category_service.dart"; import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; import "package:flutter_shopping/flutter_shopping.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; import "package:go_router/go_router.dart"; // (REQUIRED): Create your own instance of the ProductService. diff --git a/example_amazon/lib/src/models/my_category.dart b/packages/flutter_shopping/example_amazon/lib/src/models/my_category.dart similarity index 65% rename from example_amazon/lib/src/models/my_category.dart rename to packages/flutter_shopping/example_amazon/lib/src/models/my_category.dart index 50d721a..0899d37 100644 --- a/example_amazon/lib/src/models/my_category.dart +++ b/packages/flutter_shopping/example_amazon/lib/src/models/my_category.dart @@ -1,4 +1,4 @@ -import "package:flutter_product_page/flutter_product_page.dart"; +import 'package:flutter_shopping/flutter_shopping.dart'; class MyCategory extends ProductPageShop { const MyCategory({ diff --git a/example_amazon/lib/src/models/my_product.dart b/packages/flutter_shopping/example_amazon/lib/src/models/my_product.dart similarity index 75% rename from example_amazon/lib/src/models/my_product.dart rename to packages/flutter_shopping/example_amazon/lib/src/models/my_product.dart index f5a5ab4..89f8bd1 100644 --- a/example_amazon/lib/src/models/my_product.dart +++ b/packages/flutter_shopping/example_amazon/lib/src/models/my_product.dart @@ -1,5 +1,4 @@ -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; +import 'package:flutter_shopping/flutter_shopping.dart'; class MyProduct extends ShoppingCartProduct with ProductPageProduct { MyProduct({ diff --git a/example_amazon/lib/src/routes.dart b/packages/flutter_shopping/example_amazon/lib/src/routes.dart similarity index 100% rename from example_amazon/lib/src/routes.dart rename to packages/flutter_shopping/example_amazon/lib/src/routes.dart diff --git a/example_amazon/lib/src/services/category_service.dart b/packages/flutter_shopping/example_amazon/lib/src/services/category_service.dart similarity index 97% rename from example_amazon/lib/src/services/category_service.dart rename to packages/flutter_shopping/example_amazon/lib/src/services/category_service.dart index 7c74bae..d4aa2f6 100644 --- a/example_amazon/lib/src/services/category_service.dart +++ b/packages/flutter_shopping/example_amazon/lib/src/services/category_service.dart @@ -1,6 +1,6 @@ import "package:amazon/src/models/my_category.dart"; import "package:amazon/src/models/my_product.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; Map categories = { "Electronics": "Electronica", diff --git a/example_amazon/lib/src/ui/homepage.dart b/packages/flutter_shopping/example_amazon/lib/src/ui/homepage.dart similarity index 100% rename from example_amazon/lib/src/ui/homepage.dart rename to packages/flutter_shopping/example_amazon/lib/src/ui/homepage.dart diff --git a/example_amazon/lib/src/utils/go_router.dart b/packages/flutter_shopping/example_amazon/lib/src/utils/go_router.dart similarity index 100% rename from example_amazon/lib/src/utils/go_router.dart rename to packages/flutter_shopping/example_amazon/lib/src/utils/go_router.dart diff --git a/example_amazon/lib/src/utils/theme.dart b/packages/flutter_shopping/example_amazon/lib/src/utils/theme.dart similarity index 100% rename from example_amazon/lib/src/utils/theme.dart rename to packages/flutter_shopping/example_amazon/lib/src/utils/theme.dart diff --git a/example_amazon/pubspec.yaml b/packages/flutter_shopping/example_amazon/pubspec.yaml similarity index 50% rename from example_amazon/pubspec.yaml rename to packages/flutter_shopping/example_amazon/pubspec.yaml index 6d51de9..7c5ac3c 100644 --- a/example_amazon/pubspec.yaml +++ b/packages/flutter_shopping/example_amazon/pubspec.yaml @@ -1,7 +1,7 @@ name: amazon description: "A new Flutter project." publish_to: 'none' -version: 1.0.0+1 +version: 1.0.0 environment: sdk: '>=3.4.1 <4.0.0' @@ -16,22 +16,8 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_nested_categories ref: 0.0.1 - flutter_product_page: - git: - url: https://github.com/Iconica-Development/flutter_product_page - ref: 1.3.3 - flutter_shopping_cart: - git: - url: https://github.com/Iconica-Development/flutter_shopping_cart - ref: 1.1.1 - flutter_order_details: - git: - url: https://github.com/Iconica-Development/flutter_order_details - ref: 1.0.1 flutter_shopping: - git: - url: https://github.com/Iconica-Development/flutter_shopping - ref: 1.0.6 + path: ../ dev_dependencies: flutter_test: diff --git a/example_amazon/test/widget_test.dart b/packages/flutter_shopping/example_amazon/test/widget_test.dart similarity index 100% rename from example_amazon/test/widget_test.dart rename to packages/flutter_shopping/example_amazon/test/widget_test.dart diff --git a/packages/flutter_shopping/lib/flutter_shopping.dart b/packages/flutter_shopping/lib/flutter_shopping.dart new file mode 100644 index 0000000..586414f --- /dev/null +++ b/packages/flutter_shopping/lib/flutter_shopping.dart @@ -0,0 +1,11 @@ +/// Flutter Shopping +library flutter_shopping; + +export "package:flutter_order_details/flutter_order_details.dart"; +export "package:flutter_product_page/flutter_product_page.dart"; +export "package:flutter_shopping_cart/flutter_shopping_cart.dart"; + +export "src/config/flutter_shopping_configuration.dart"; +export "src/routes.dart"; +export "src/user_stores/flutter_shopping_userstory_go_router.dart"; +export "src/user_stores/flutter_shopping_userstory_navigation.dart"; diff --git a/lib/src/config/default_order_detail_configuration.dart b/packages/flutter_shopping/lib/src/config/default_order_detail_configuration.dart similarity index 97% rename from lib/src/config/default_order_detail_configuration.dart rename to packages/flutter_shopping/lib/src/config/default_order_detail_configuration.dart index 7e24488..67dfd81 100644 --- a/lib/src/config/default_order_detail_configuration.dart +++ b/packages/flutter_shopping/lib/src/config/default_order_detail_configuration.dart @@ -1,5 +1,4 @@ import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; import "package:flutter_shopping/flutter_shopping.dart"; import "package:go_router/go_router.dart"; diff --git a/lib/src/config/flutter_shopping_configuration.dart b/packages/flutter_shopping/lib/src/config/flutter_shopping_configuration.dart similarity index 100% rename from lib/src/config/flutter_shopping_configuration.dart rename to packages/flutter_shopping/lib/src/config/flutter_shopping_configuration.dart diff --git a/lib/src/go_router.dart b/packages/flutter_shopping/lib/src/go_router.dart similarity index 100% rename from lib/src/go_router.dart rename to packages/flutter_shopping/lib/src/go_router.dart diff --git a/lib/src/routes.dart b/packages/flutter_shopping/lib/src/routes.dart similarity index 100% rename from lib/src/routes.dart rename to packages/flutter_shopping/lib/src/routes.dart diff --git a/lib/src/user_stores/flutter_shopping_userstory_go_router.dart b/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_go_router.dart similarity index 96% rename from lib/src/user_stores/flutter_shopping_userstory_go_router.dart rename to packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_go_router.dart index 7041f6d..46ca675 100644 --- a/lib/src/user_stores/flutter_shopping_userstory_go_router.dart +++ b/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_go_router.dart @@ -1,4 +1,3 @@ -import "package:flutter_order_details/flutter_order_details.dart"; import "package:flutter_shopping/flutter_shopping.dart"; import "package:flutter_shopping/src/config/default_order_detail_configuration.dart"; import "package:flutter_shopping/src/widgets/default_order_failed_widget.dart"; diff --git a/lib/src/user_stores/flutter_shopping_userstory_navigation.dart b/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_navigation.dart similarity index 95% rename from lib/src/user_stores/flutter_shopping_userstory_navigation.dart rename to packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_navigation.dart index 24670ae..52ca23f 100644 --- a/lib/src/user_stores/flutter_shopping_userstory_navigation.dart +++ b/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_navigation.dart @@ -1,5 +1,4 @@ import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; import "package:flutter_shopping/flutter_shopping.dart"; import "package:go_router/go_router.dart"; diff --git a/lib/src/widgets/default_order_failed_widget.dart b/packages/flutter_shopping/lib/src/widgets/default_order_failed_widget.dart similarity index 100% rename from lib/src/widgets/default_order_failed_widget.dart rename to packages/flutter_shopping/lib/src/widgets/default_order_failed_widget.dart diff --git a/lib/src/widgets/default_order_succes_widget.dart b/packages/flutter_shopping/lib/src/widgets/default_order_succes_widget.dart similarity index 100% rename from lib/src/widgets/default_order_succes_widget.dart rename to packages/flutter_shopping/lib/src/widgets/default_order_succes_widget.dart diff --git a/packages/flutter_shopping/pubspec.yaml b/packages/flutter_shopping/pubspec.yaml new file mode 100644 index 0000000..762301a --- /dev/null +++ b/packages/flutter_shopping/pubspec.yaml @@ -0,0 +1,47 @@ +name: flutter_shopping +description: "A new Flutter project." +publish_to: 'none' # Remove this line if you wish to publish to pub.dev +version: 1.0.0 + +environment: + sdk: '>=3.3.4 <4.0.0' + +dependencies: + flutter: + sdk: flutter + go_router: any + flutter_product_page: + git: + url: https://github.com/Iconica-Development/flutter_shopping + ref: 1.0.0 + path: packages/flutter_product_page + flutter_shopping_cart: + git: + url: https://github.com/Iconica-Development/flutter_shopping + ref: 1.0.0 + path: packages/flutter_shopping_cart + flutter_order_details: + git: + url: https://github.com/Iconica-Development/flutter_shopping + ref: 1.0.0 + path: packages/flutter_order_details + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 7.0.0 + +flutter: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic diff --git a/packages/flutter_shopping_cart/.gitignore b/packages/flutter_shopping_cart/.gitignore new file mode 100644 index 0000000..e31020f --- /dev/null +++ b/packages/flutter_shopping_cart/.gitignore @@ -0,0 +1,49 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ +.metadata + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# env +*dotenv diff --git a/packages/flutter_shopping_cart/README.md b/packages/flutter_shopping_cart/README.md new file mode 100644 index 0000000..c9ededd --- /dev/null +++ b/packages/flutter_shopping_cart/README.md @@ -0,0 +1,77 @@ +# flutter_shopping_cart + +This component contains a shopping cart screen and the functionality for shopping carts that contain products. + +## Features + +* Shopping cart screen +* Shopping cart products + +## Usage + +First, create your own product by extending the `Product` class: + +```dart +class ExampleProduct extends Product { + ExampleProduct({ + requried super.id, + required super.name, + required super.price, + required this.image, + super.quantity, + }); + + final String image; +} +``` + +Next, you can create the `ShoppingCartScreen` widget like this: + +```dart +var myProductService = ProductService([]); + +ShoppingCartScreen( + configuration: ShoppingCartConfig( + productService: myProductService, + // + productItemBuilder: ( + BuildContext context, + Locale locale, + ExampleProduct product, + ) => + ListTile( + title: Text(product.name), + subtitle: Text(product.price.toString()), + ), + // + onConfirmOrder: (List products) { + print("Placing order with products: $products"); + }, + ), +); +``` + +For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping_cart/tree/main/example). + +Or, you could run the example yourself: +``` +git clone https://github.com/Iconica-Development/flutter_shopping_cart.git + +cd flutter_shopping_cart + +cd example + +flutter run +``` + +## Issues + +Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_shopping_cart) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). + +## Want to contribute + +If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_shopping_cart/pulls). + +## Author + +This flutter_shopping_cart for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at diff --git a/packages/flutter_shopping_cart/analysis_options.yaml b/packages/flutter_shopping_cart/analysis_options.yaml new file mode 100644 index 0000000..0736605 --- /dev/null +++ b/packages/flutter_shopping_cart/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_iconica_analysis/components_options.yaml + +# Possible to overwrite the rules from the package + +analyzer: + exclude: + +linter: + rules: diff --git a/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart b/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart new file mode 100644 index 0000000..b5fdb71 --- /dev/null +++ b/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart @@ -0,0 +1,8 @@ +/// Flutter component for shopping cart. +library flutter_shopping_cart; + +export "src/config/shopping_cart_config.dart"; +export "src/config/shopping_cart_localizations.dart"; +export "src/models/shopping_cart_product.dart"; +export "src/services/product_service.dart"; +export "src/widgets/shopping_cart_screen.dart"; diff --git a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart new file mode 100644 index 0000000..852d124 --- /dev/null +++ b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart @@ -0,0 +1,133 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; + +Widget _defaultNoContentBuilder(BuildContext context) => + const SizedBox.shrink(); + +/// Shopping cart configuration +/// +/// This class is used to configure the shopping cart. +class ShoppingCartConfig { + /// Creates a shopping cart configuration. + ShoppingCartConfig({ + required this.productService, + // + this.onConfirmOrder, + this.confirmOrderButtonBuilder, + this.confirmOrderButtonHeight = 100, + // + this.sumBottomSheetBuilder, + this.sumBottomSheetHeight = 100, + // + this.title, + this.titleBuilder, + // + this.localizations = const ShoppingCartLocalizations(), + // + this.padding = const EdgeInsets.symmetric(horizontal: 32), + this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32), + // + this.appBar, + // + Widget Function(BuildContext context, Locale locale, T product)? + productItemBuilder, + Widget Function(BuildContext context) noContentBuilder = + _defaultNoContentBuilder, + }) : assert( + confirmOrderButtonBuilder != null || onConfirmOrder != null, + """ +If you override the confirm order button builder, +you cannot use the onConfirmOrder callback.""", + ), + assert( + confirmOrderButtonBuilder == null || onConfirmOrder == null, + """ +If you do not override the confirm order button builder, +you must use the onConfirmOrder callback.""", + ), + _noContentBuilder = noContentBuilder { + _productItemBuilder = productItemBuilder; + _productItemBuilder ??= (context, locale, product) => ListTile( + title: Text(product.name), + subtitle: Text(product.price.toString()), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () => productService.removeProduct(product), + ), + ); + } + + /// Product Service. The service contains all the products that + /// a shopping cart can contain. Each product must extend the [Product] class. + /// The service is used to add, remove, and update products. + /// + /// The service can be seperate for each shopping cart in-case you want to + /// support seperate shopping carts for shop. + ProductService productService = ProductService([]); + + late final Widget Function(BuildContext context, Locale locale, T product)? + _productItemBuilder; + + /// Product item builder. This builder is used to build the product item + /// that will be displayed in the shopping cart. + Widget Function(BuildContext context, Locale locale, T product) + get productItemBuilder => _productItemBuilder!; + + final Widget Function(BuildContext context) _noContentBuilder; + + /// No content builder. This builder is used to build the no content widget + /// that will be displayed in the shopping cart when there are no products. + Widget Function(BuildContext context) get noContentBuilder => + _noContentBuilder; + + /// Confirm order button builder. This builder is used to build the confirm + /// order button that will be displayed in the shopping cart. + /// If you override this builder, you cannot use the [onConfirmOrder] callback + final Widget Function(BuildContext context)? confirmOrderButtonBuilder; + + /// Confirm order button height. The height of the confirm order button. + /// This height is used to calculate the bottom padding of the shopping cart. + /// If you override the confirm order button builder, you must provide a + /// height. + final double confirmOrderButtonHeight; + + /// Confirm order callback. This callback is called when the confirm order + /// button is pressed. The callback will not be called if you override the + /// confirm order button builder. + final Function(List products)? onConfirmOrder; + + /// Sum bottom sheet builder. This builder is used to build the sum bottom + /// sheet that will be displayed in the shopping cart. The sum bottom sheet + /// can be used to display the total sum of the products in the shopping cart. + final Widget Function(BuildContext context)? sumBottomSheetBuilder; + + /// Sum bottom sheet height. The height of the sum bottom sheet. + /// This height is used to calculate the bottom padding of the shopping cart. + /// If you override the sum bottom sheet builder, you must provide a height. + final double sumBottomSheetHeight; + + /// Padding around the shopping cart. The padding is used to create space + /// around the shopping cart. + final EdgeInsets padding; + + /// Bottom padding of the shopping cart. The bottom padding is used to create + /// a padding around the bottom sheet. This padding is ignored when the + /// [sumBottomSheetBuilder] is overridden. + final EdgeInsets bottomPadding; + + /// Title of the shopping cart. The title is displayed at the top of the + /// shopping cart. If you provide a title builder, the title will be ignored. + final String? title; + + /// Title builder. This builder is used to build the title of the shopping + /// cart. The title is displayed at the top of the shopping cart. If you + /// use the title builder, the [title] will be ignored. + final Widget Function(BuildContext context)? titleBuilder; + + /// Shopping cart localizations. The localizations are used to localize the + /// shopping cart. + final ShoppingCartLocalizations localizations; + + /// App bar for the shopping cart screen. + final PreferredSizeWidget? appBar; +} diff --git a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart new file mode 100644 index 0000000..19122d6 --- /dev/null +++ b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart @@ -0,0 +1,24 @@ +import "package:flutter/material.dart"; + +/// Shopping cart localizations +class ShoppingCartLocalizations { + /// Creates shopping cart localizations + const ShoppingCartLocalizations({ + this.locale = const Locale("en", "US"), + this.placeOrder = "PLACE ORDER", + this.sum = "Total:", + }); + + /// Locale for the shopping cart. + /// This locale will be used to format the currency. + /// Default is English. + final Locale locale; + + /// Localization for the place order button. + /// This text will only be displayed if you're not using the place order + /// button builder. + final String placeOrder; + + /// Localization for the sum. + final String sum; +} diff --git a/packages/flutter_shopping_cart/lib/src/models/shopping_cart_product.dart b/packages/flutter_shopping_cart/lib/src/models/shopping_cart_product.dart new file mode 100644 index 0000000..41043b0 --- /dev/null +++ b/packages/flutter_shopping_cart/lib/src/models/shopping_cart_product.dart @@ -0,0 +1,29 @@ +/// Abstract class for Product +/// +/// All products that want to be added to the shopping cart +/// must extend this class. +abstract class ShoppingCartProduct { + /// Creates a new product. + ShoppingCartProduct({ + required this.id, + required this.name, + required this.price, + this.quantity = 1, + }); + + /// Unique product identifier. + /// This identifier will be used to identify the product in the shopping cart. + /// If you don't provide an identifier, a random identifier will be generated. + final String id; + + /// Product name. + /// This name will be displayed in the shopping cart. + final String name; + + /// Product price. + /// This price will be displayed in the shopping cart. + final double price; + + /// Quantity for the product. + int quantity; +} diff --git a/packages/flutter_shopping_cart/lib/src/services/product_service.dart b/packages/flutter_shopping_cart/lib/src/services/product_service.dart new file mode 100644 index 0000000..cb27ccc --- /dev/null +++ b/packages/flutter_shopping_cart/lib/src/services/product_service.dart @@ -0,0 +1,71 @@ +import "package:flutter/foundation.dart"; +import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; + +/// Product service. This class is responsible for managing the products. +/// The service is used to add, remove, and update products. +class ProductService extends ChangeNotifier { + /// Creates a product service. + ProductService(this.products); + + /// List of products in the shopping cart. + final List products; + + /// Adds a product to the shopping cart. + void addProduct(T product) { + for (var p in products) { + if (p.id == product.id) { + p.quantity++; + notifyListeners(); + return; + } + } + + products.add(product); + notifyListeners(); + } + + /// Removes a product from the shopping cart. + void removeProduct(T product) { + for (var p in products) { + if (p.id == product.id) { + products.remove(p); + notifyListeners(); + return; + } + } + notifyListeners(); + } + + /// Removes one product from the shopping cart. + void removeOneProduct(T product) { + for (var p in products) { + if (p.id == product.id) { + if (p.quantity > 1) { + p.quantity--; + notifyListeners(); + return; + } + } + } + + products.remove(product); + notifyListeners(); + } + + /// Counts the number of products in the shopping cart. + int countProducts() { + var count = 0; + + for (var product in products) { + count += product.quantity; + } + + return count; + } + + /// Empties the shopping cart. + void clear() { + products.clear(); + notifyListeners(); + } +} diff --git a/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart b/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart new file mode 100644 index 0000000..1badb7e --- /dev/null +++ b/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart @@ -0,0 +1,232 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; + +/// Shopping cart screen widget. +class ShoppingCartScreen + extends StatelessWidget { + /// Creates a shopping cart screen. + const ShoppingCartScreen({ + required this.configuration, + super.key, + }); + + /// Configuration for the shopping cart screen. + final ShoppingCartConfig configuration; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var productBuilder = SingleChildScrollView( + child: Column( + children: [ + if (configuration.titleBuilder != null) ...{ + configuration.titleBuilder!(context), + } else if (configuration.title != null) ...{ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Text( + configuration.title!, + style: theme.textTheme.titleLarge, + ), + ), + }, + ListenableBuilder( + listenable: configuration.productService, + builder: (context, _) { + var products = configuration.productService.products; + + if (products.isEmpty) { + return configuration.noContentBuilder(context); + } + + return Column( + children: [ + for (var product in products) + configuration.productItemBuilder( + context, + configuration.localizations.locale, + product, + ), + // Additional whitespace at the bottom to make sure the + // last product(s) are not hidden by the bottom sheet. + SizedBox( + height: configuration.confirmOrderButtonHeight + + configuration.sumBottomSheetHeight, + ), + ], + ); + }, + ), + ], + ), + ); + + var bottomHeight = configuration.confirmOrderButtonHeight + + configuration.sumBottomSheetHeight; + + var bottomBlur = Container( + height: bottomHeight, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.surface.withOpacity(0), + theme.colorScheme.surface.withOpacity(.5), + theme.colorScheme.surface.withOpacity(.8), + theme.colorScheme.surface.withOpacity(.8), + theme.colorScheme.surface.withOpacity(.8), + theme.colorScheme.surface.withOpacity(.8), + theme.colorScheme.surface.withOpacity(1), + ], + ), + ), + ); + + return Scaffold( + appBar: configuration.appBar, + body: Stack( + fit: StackFit.expand, + children: [ + Padding( + padding: configuration.padding, + child: productBuilder, + ), + Align( + alignment: Alignment.bottomCenter, + child: bottomBlur, + ), + Align( + alignment: Alignment.bottomCenter, + child: _BottomSheet( + configuration: configuration, + ), + ), + ], + ), + ); + } +} + +class _BottomSheet extends StatelessWidget { + const _BottomSheet({ + required this.configuration, + super.key, + }); + + final ShoppingCartConfig configuration; + + @override + Widget build(BuildContext context) { + var placeOrderButton = ListenableBuilder( + listenable: configuration.productService, + builder: (BuildContext context, Widget? child) => + configuration.confirmOrderButtonBuilder != null + ? configuration.confirmOrderButtonBuilder!(context) + : _DefaultConfirmOrderButton(configuration: configuration), + ); + + var bottomSheet = ListenableBuilder( + listenable: configuration.productService, + builder: (BuildContext context, Widget? child) => + configuration.sumBottomSheetBuilder != null + ? configuration.sumBottomSheetBuilder!(context) + : _DefaultSumBottomSheet(configuration: configuration), + ); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + bottomSheet, + placeOrderButton, + ], + ); + } +} + +class _DefaultConfirmOrderButton + extends StatelessWidget { + const _DefaultConfirmOrderButton({ + required this.configuration, + }); + + final ShoppingCartConfig configuration; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + void onConfirmOrderPressed(List products) { + if (configuration.onConfirmOrder == null) { + return; + } + + if (products.isEmpty) { + return; + } + + configuration.onConfirmOrder!(products); + } + + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 80), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + ), + onPressed: () => onConfirmOrderPressed( + configuration.productService.products, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Text( + """${configuration.localizations.placeOrder} (${configuration.productService.countProducts()})""", + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onPrimary, + ), + ), + ), + ), + ), + ); + } +} + +class _DefaultSumBottomSheet extends StatelessWidget { + const _DefaultSumBottomSheet({ + required this.configuration, + }); + + final ShoppingCartConfig configuration; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var totalPrice = configuration.productService.products + .map((product) => product.price * product.quantity) + .fold(0.0, (a, b) => a + b); + + return Padding( + padding: configuration.bottomPadding, + child: Row( + children: [ + Text( + configuration.localizations.sum, + style: theme.textTheme.titleMedium, + ), + const Spacer(), + Text( + totalPrice.toStringAsFixed(2), + style: theme.textTheme.titleMedium, + ), + ], + ), + ); + } +} diff --git a/packages/flutter_shopping_cart/pubspec.yaml b/packages/flutter_shopping_cart/pubspec.yaml new file mode 100644 index 0000000..119d182 --- /dev/null +++ b/packages/flutter_shopping_cart/pubspec.yaml @@ -0,0 +1,21 @@ +name: flutter_shopping_cart +description: "A Flutter module for a shopping cart." +version: 1.0.0 + +environment: + sdk: '>=3.3.0 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 7.0.0 + +flutter: \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index e4c7983..d9e7eb6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,44 +1,6 @@ -name: flutter_shopping -description: "A new Flutter project." -publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.0+1 +name: flutter_shopping_workspace environment: - sdk: '>=3.3.4 <4.0.0' - -dependencies: - flutter: - sdk: flutter - go_router: any - flutter_product_page: - git: - url: https://github.com/Iconica-Development/flutter_product_page - ref: 1.3.3 - flutter_shopping_cart: - git: - url: https://github.com/Iconica-Development/flutter_shopping_cart - ref: 1.1.1 - flutter_order_details: - git: - url: https://github.com/Iconica-Development/flutter_order_details - ref: 1.0.1 - + sdk: ">=3.1.0 <4.0.0" dev_dependencies: - flutter_test: - sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 - -flutter: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic + melos: ^6.1.0