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
This commit is contained in:
Freek van de Ven 2024-06-26 16:12:48 +02:00 committed by GitHub
parent 3b51c6ca53
commit 0838b7b017
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
112 changed files with 4330 additions and 553 deletions

22
.github/dependabot.yml vendored Normal file
View file

@ -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"

View file

@ -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

14
.github/workflows/melos-ci.yml vendored Normal file
View file

@ -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

43
.gitignore vendored
View file

@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2024 Iconica
#
# SPDX-License-Identifier: GPL-3.0-or-later
# Miscellaneous # Miscellaneous
*.class *.class
*.log *.log
@ -9,13 +13,13 @@
.history .history
.svn/ .svn/
migrate_working_dir/ migrate_working_dir/
.metadata
# IntelliJ related # IntelliJ related
*.iml *.iml
*.ipr *.ipr
*.iws *.iws
.idea/ .idea/
ios
# The .vscode folder contains launch configuration and tasks you configure in # 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 # VS Code which you may wish to be included in version control, so this line
@ -23,33 +27,20 @@ migrate_working_dir/
.vscode/ .vscode/
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
# /pubspec.lock
**/doc/api/ **/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/ .dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages .packages
.pub-cache/ build/
.pub/ .flutter-plugins-dependencies
/build/ .flutter-plugins
.metadata
pubspec.lock 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 pubspec_overrides.yaml
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/

3
CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
## 1.0.0
- Initial version of the combined melos variant of the flutter_shopping user-story.

9
LICENSE Normal file
View file

@ -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.

View file

@ -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";

1
linux/.gitignore vendored
View file

@ -1 +0,0 @@
flutter/ephemeral

View file

@ -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 "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>: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()

View file

@ -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}
)

View file

@ -1,11 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

View file

@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -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 $<TARGET_FILE:${plugin}_plugin>)
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)

View file

@ -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);
}

View file

@ -1,124 +0,0 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#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));
}

View file

@ -1,18 +0,0 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
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_

39
melos.yaml Normal file
View file

@ -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.

View file

@ -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

View file

@ -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 <support@iconica.nl>

View file

@ -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";

View file

@ -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<OrderDetailStep> 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;
}

View file

@ -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;
}

View file

@ -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<FormState> formKey;
/// List of fields that the user has to fill in.
/// Each field must extend from the `OrderDetailInput` class.
final List<OrderDetailInput> fields;
}

View file

@ -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,
}

View file

@ -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,
),
);
}
}

View file

@ -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<String> {
/// 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<String? Function(String?)>? streetNameValidators;
/// Validators for the postal code.
final List<String? Function(String?)>? postalCodeValidators;
/// Validators for the city.
final List<String? Function(String?)>? cityValidators;
/// Input formatters for the postal code.
final List<TextInputFormatter>? 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,
);
}
}

View file

@ -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<String> {
/// 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<String> 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<T> extends FormField<T> {
_ChoiceInputField({
required T currentValue,
required List<T> 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<T> field) => Padding(
padding: fieldPadding,
child: Column(
children: [
for (var item in items) ...[
Padding(
padding: paddingBetweenFields,
child: _InputContent<T>(
i: item,
currentValue: currentValue,
onTap: onTap,
),
),
],
if (field.hasError) ...[
FormFieldErrorBuilder(errorMessage: field.errorText!),
],
],
),
),
);
}
class _InputContent<T> 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,
);
}
}

View file

@ -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<T> extends OrderDetailInput<T> {
/// 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<T> 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<T>(
value: currentValue ?? initialValue ?? buildInitialValue ?? items[0],
selectedItemBuilder: (context) => items
.map(
(item) => Text(
item.toString(),
style: theme.textTheme.labelMedium,
),
)
.toList(),
items: items
.map(
(item) => DropdownMenuItem<T>(
value: item,
child: _DropdownButtonBuilder<T>(
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<T> 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,
],
],
),
);
}
}

View file

@ -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<String> {
/// 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,
);
}
}

View file

@ -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<T> {
/// 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<String? Function(T?)> 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<FormField>) ...[
Column(
children: child
.map(
(FormField field) => Padding(
padding: paddingBetweenFields,
child: field,
),
)
.toList(),
),
] else if (child is List<OrderDetailInput>) ...[
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,
);
}

View file

@ -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<String> {
/// 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,
);
}
}

View file

@ -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<String, dynamic> order;
}

View file

@ -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<String> {
/// 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<TextInputFormatter> 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,
);
}
}

View file

@ -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<String> {
/// 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<String>(
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<T> extends FormField<T> {
_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<T> 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<T> 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;
}

View file

@ -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<OrderDetailScreen> createState() => _OrderDetailScreenState();
}
class _OrderDetailScreenState extends State<OrderDetailScreen> {
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,
),
),
],
);
}
}

View file

@ -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

View file

@ -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/

View file

@ -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 <support@iconica.nl>

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/components_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -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";

View file

@ -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;
}

View file

@ -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<List<ProductPageShop>> shops;
/// A function that returns all the products that belong to a certain shop.
/// The function must return a [ProductPageContent] object.
final Future<ProductPageContent> 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;
}

View file

@ -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<ProductPageProduct> products;
/// Optional highlighted discounted product to display.
final ProductPageProduct? discountedProduct;
}

View file

@ -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;
}

View file

@ -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,
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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<ProductPageProduct> products,
) {
var categorizedProducts = <String, List<ProductPageProduct>>{};
for (var product in products) {
if (!categorizedProducts.containsKey(product.category)) {
categorizedProducts[product.category] = [];
}
categorizedProducts[product.category]?.add(product);
}
// Create Category instances
var categories = <Category>[];
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,
);
}

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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),
),
),
),
),
),
);
}
}

View file

@ -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<ProductPageShop> 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,
);
}
}

View file

@ -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,
],
),
),
),
);
}
}

View file

@ -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<ProductPageShop>? 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<ProductPageShop> 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,
],
);
},
),
);
}

View file

@ -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,
);
}

View file

@ -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<ProductPageShop> 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(),
),
);
}
}

View file

@ -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,
],
),
),
);
}
}

View file

@ -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<ProductPageShop> 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<int> currentRow,
double availableRowLength,
) {
var theme = Theme.of(context);
var row = <Widget>[];
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<Row> _buildButtonRows(BuildContext context) {
var theme = Theme.of(context);
var rows = <Row>[];
var currentRow = <int>[];
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 = <int>[];
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,
),
);
}

View file

@ -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

56
packages/flutter_shopping/.gitignore vendored Normal file
View file

@ -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/

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/components_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -3,10 +3,7 @@ import "package:example/src/routes.dart";
import "package:example/src/services/order_service.dart"; import "package:example/src/services/order_service.dart";
import "package:example/src/services/shop_service.dart"; import "package:example/src/services/shop_service.dart";
import "package:flutter/material.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/flutter_shopping.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
import "package:go_router/go_router.dart"; import "package:go_router/go_router.dart";
// (REQUIRED): Create your own instance of the ProductService. // (REQUIRED): Create your own instance of the ProductService.

View file

@ -1,5 +1,4 @@
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";
class MyProduct extends ShoppingCartProduct with ProductPageProduct { class MyProduct extends ShoppingCartProduct with ProductPageProduct {
MyProduct({ MyProduct({

View file

@ -1,4 +1,4 @@
import "package:flutter_product_page/flutter_product_page.dart"; import "package:flutter_shopping/flutter_shopping.dart";
class MyShop extends ProductPageShop { class MyShop extends ProductPageShop {
const MyShop({ const MyShop({

View file

@ -1,5 +1,5 @@
import "package:example/src/models/my_product.dart"; 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. /// Example implementation of storing an order in a database.
void storeOrderInDatabase(List<MyProduct> products, OrderResult result) { void storeOrderInDatabase(List<MyProduct> products, OrderResult result) {

View file

@ -1,6 +1,6 @@
import "package:example/src/models/my_product.dart"; import "package:example/src/models/my_product.dart";
import "package:example/src/models/my_shop.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 /// This function should have your own implementation. Generally this would
/// contain some API call to fetch the list of shops. /// contain some API call to fetch the list of shops.

View file

@ -1,7 +1,7 @@
name: example name: example
description: Demonstrates how to use the flutter_shopping package." description: Demonstrates how to use the flutter_shopping package."
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1 version: 1.0.0
environment: environment:
sdk: '>=3.3.4 <4.0.0' sdk: '>=3.3.4 <4.0.0'
@ -19,20 +19,6 @@ dependencies:
flutter_shopping: flutter_shopping:
path: ../ 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: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View file

@ -2,9 +2,7 @@ import "package:amazon/src/models/my_product.dart";
import "package:amazon/src/routes.dart"; import "package:amazon/src/routes.dart";
import "package:amazon/src/services/category_service.dart"; import "package:amazon/src/services/category_service.dart";
import "package:flutter/material.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/flutter_shopping.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
import "package:go_router/go_router.dart"; import "package:go_router/go_router.dart";
// (REQUIRED): Create your own instance of the ProductService. // (REQUIRED): Create your own instance of the ProductService.

View file

@ -1,4 +1,4 @@
import "package:flutter_product_page/flutter_product_page.dart"; import 'package:flutter_shopping/flutter_shopping.dart';
class MyCategory extends ProductPageShop { class MyCategory extends ProductPageShop {
const MyCategory({ const MyCategory({

View file

@ -1,5 +1,4 @@
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";
class MyProduct extends ShoppingCartProduct with ProductPageProduct { class MyProduct extends ShoppingCartProduct with ProductPageProduct {
MyProduct({ MyProduct({

View file

@ -1,6 +1,6 @@
import "package:amazon/src/models/my_category.dart"; import "package:amazon/src/models/my_category.dart";
import "package:amazon/src/models/my_product.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<String, String> categories = { Map<String, String> categories = {
"Electronics": "Electronica", "Electronics": "Electronica",

View file

@ -1,7 +1,7 @@
name: amazon name: amazon
description: "A new Flutter project." description: "A new Flutter project."
publish_to: 'none' publish_to: 'none'
version: 1.0.0+1 version: 1.0.0
environment: environment:
sdk: '>=3.4.1 <4.0.0' sdk: '>=3.4.1 <4.0.0'
@ -16,22 +16,8 @@ dependencies:
git: git:
url: https://github.com/Iconica-Development/flutter_nested_categories url: https://github.com/Iconica-Development/flutter_nested_categories
ref: 0.0.1 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: flutter_shopping:
git: path: ../
url: https://github.com/Iconica-Development/flutter_shopping
ref: 1.0.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -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";

View file

@ -1,5 +1,4 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_shopping/flutter_shopping.dart"; import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart"; import "package:go_router/go_router.dart";

View file

@ -1,4 +1,3 @@
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_shopping/flutter_shopping.dart"; import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping/src/config/default_order_detail_configuration.dart"; import "package:flutter_shopping/src/config/default_order_detail_configuration.dart";
import "package:flutter_shopping/src/widgets/default_order_failed_widget.dart"; import "package:flutter_shopping/src/widgets/default_order_failed_widget.dart";

View file

@ -1,5 +1,4 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_shopping/flutter_shopping.dart"; import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart"; import "package:go_router/go_router.dart";

Some files were not shown because too many files have changed in this diff Show more