mirror of
https://github.com/Iconica-Development/flutter_shopping.git
synced 2025-05-19 08:53:46 +02:00
feat: add new userstory structure
This commit is contained in:
parent
88cefb047b
commit
efca65b6be
120 changed files with 3569 additions and 4560 deletions
|
@ -1,3 +1,8 @@
|
||||||
|
## 3.0.0
|
||||||
|
- Refactored the project structure
|
||||||
|
- Added `flutter_shopping_interface` package
|
||||||
|
- Implemented default design
|
||||||
|
|
||||||
## 2.0.0
|
## 2.0.0
|
||||||
- Added `flutter_shopping_interface` package
|
- Added `flutter_shopping_interface` package
|
||||||
- Implemented default design
|
- Implemented default design
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
migrate_working_dir/
|
migrate_working_dir/
|
||||||
.metadata
|
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
|
@ -20,30 +19,11 @@ migrate_working_dir/
|
||||||
# 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
|
||||||
# is commented out by default.
|
# is commented out by default.
|
||||||
.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
|
build/
|
||||||
.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
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
library firebase_shopping_repository;
|
||||||
|
|
||||||
|
export 'src/firebase_category_repository.dart';
|
||||||
|
export 'src/firebase_product_repository.dart';
|
||||||
|
export 'src/firebase_shop_repository.dart';
|
||||||
|
export 'src/firebase_shopping_cart_repository.dart';
|
|
@ -0,0 +1,53 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
import 'package:shopping_repository_interface/shopping_repository_interface.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
class FirebaseCategoryRepository implements CategoryRepositoryInterface {
|
||||||
|
final StreamController<List<Category>> _categoryController =
|
||||||
|
BehaviorSubject<List<Category>>();
|
||||||
|
|
||||||
|
final StreamController<List<Category>> _selectedCategoriesController =
|
||||||
|
BehaviorSubject<List<Category>>();
|
||||||
|
|
||||||
|
List<Category> _selectedCategories = [];
|
||||||
|
|
||||||
|
List<Category> _categories = [];
|
||||||
|
@override
|
||||||
|
void deselectCategory(String? categoryId) {
|
||||||
|
_selectedCategories.removeWhere((category) => category.id == categoryId);
|
||||||
|
_selectedCategoriesController.add(_selectedCategories);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<Category>> getCategories() {
|
||||||
|
FirebaseFirestore.instance
|
||||||
|
.collection('shopping_category')
|
||||||
|
.snapshots()
|
||||||
|
.listen((event) {
|
||||||
|
List<Category> categories = [];
|
||||||
|
event.docs.forEach((element) {
|
||||||
|
categories.add(Category.fromMap(element.id, element.data()));
|
||||||
|
});
|
||||||
|
_categoryController.add(categories);
|
||||||
|
_categories = categories;
|
||||||
|
});
|
||||||
|
|
||||||
|
return _categoryController.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<Category>?> getSelectedCategoryStream() {
|
||||||
|
_selectedCategoriesController.add(_selectedCategories);
|
||||||
|
return _selectedCategoriesController.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Category? selectCategory(String? categoryId) {
|
||||||
|
_selectedCategories
|
||||||
|
.add(_categories.firstWhere((category) => category.id == categoryId));
|
||||||
|
_selectedCategoriesController.add(_selectedCategories);
|
||||||
|
return _selectedCategories.last;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
import 'package:shopping_repository_interface/shopping_repository_interface.dart';
|
||||||
|
|
||||||
|
class FirebaseProductRepository implements ProductRepositoryInterface {
|
||||||
|
/// Shop one product
|
||||||
|
|
||||||
|
final List<Product> _products = [];
|
||||||
|
|
||||||
|
Product? _selectedProduct;
|
||||||
|
|
||||||
|
final StreamController<List<Product>> _productStream =
|
||||||
|
BehaviorSubject<List<Product>>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Product? getProduct(String? productId) =>
|
||||||
|
_products.firstWhere((product) => product.id == productId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<Product>> getProducts(List<Category>? categories, String shopId) {
|
||||||
|
FirebaseFirestore.instance
|
||||||
|
.collection('shopping_products')
|
||||||
|
.doc(shopId)
|
||||||
|
.snapshots()
|
||||||
|
.listen((event) {
|
||||||
|
_products.clear();
|
||||||
|
|
||||||
|
if (event.data() == null) return;
|
||||||
|
var shopProducts = event.data()!['products'] as List<dynamic>;
|
||||||
|
print(categories);
|
||||||
|
if (categories != null && categories.isNotEmpty)
|
||||||
|
shopProducts = shopProducts
|
||||||
|
.where(
|
||||||
|
(product) => categories
|
||||||
|
.any((category) => category.name == product['category']),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
shopProducts.forEach((product) {
|
||||||
|
_products.add(Product.fromMap(product));
|
||||||
|
});
|
||||||
|
_productStream.add(_products);
|
||||||
|
});
|
||||||
|
|
||||||
|
return _productStream.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Product? selectProduct(String? productId) {
|
||||||
|
_selectedProduct =
|
||||||
|
_products.firstWhere((product) => product.id == productId);
|
||||||
|
return _selectedProduct;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Product? getWeeklyOffer() =>
|
||||||
|
_products.firstWhereOrNull((product) => product.isDiscounted);
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
import 'package:shopping_repository_interface/shopping_repository_interface.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
class FirebaseShopRepository implements ShopRepositoryInterface {
|
||||||
|
final StreamController<List<Shop>> _shopController =
|
||||||
|
BehaviorSubject<List<Shop>>();
|
||||||
|
|
||||||
|
Shop? _selectedShop;
|
||||||
|
|
||||||
|
List<Shop> _shops = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Shop? getSelectedShop() {
|
||||||
|
return _selectedShop;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Shop? getShop(String? shopId) {
|
||||||
|
return _shops.firstWhere((element) => element.id == shopId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<Shop>> getShops() {
|
||||||
|
FirebaseFirestore.instance
|
||||||
|
.collection('shopping_shop')
|
||||||
|
.snapshots()
|
||||||
|
.listen((event) {
|
||||||
|
List<Shop> shops = [];
|
||||||
|
event.docs.forEach((element) {
|
||||||
|
shops.add(Shop.fromMap(element.id, element.data()));
|
||||||
|
});
|
||||||
|
_shops = shops;
|
||||||
|
_shopController.add(shops);
|
||||||
|
});
|
||||||
|
|
||||||
|
return _shopController.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Shop? selectShop(String? shopId) {
|
||||||
|
_selectedShop = _shops.firstWhere((element) => element.id == shopId);
|
||||||
|
return _selectedShop;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
import 'package:shopping_repository_interface/shopping_repository_interface.dart';
|
||||||
|
|
||||||
|
class FirebaseShoppingCartRepository
|
||||||
|
implements ShoppingCartRepositoryInterface {
|
||||||
|
var _cart = ShoppingCart(id: "1", products: []);
|
||||||
|
|
||||||
|
final StreamController<ShoppingCart> _shoppingCartController =
|
||||||
|
BehaviorSubject<ShoppingCart>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> addProductToCart(Product product) async {
|
||||||
|
var existingProducts = _cart.products;
|
||||||
|
var index = existingProducts.indexWhere((p) => p.id == product.id);
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
existingProducts[index] = product.copyWith(
|
||||||
|
selectedAmount: existingProducts[index].selectedAmount + 1,
|
||||||
|
);
|
||||||
|
_cart = _cart.copyWith(
|
||||||
|
products: existingProducts,
|
||||||
|
totalAmount: _cart.totalAmount + product.price,
|
||||||
|
totalAmountWithDiscount: product.isDiscounted
|
||||||
|
? _cart.totalAmountWithDiscount + product.discountPrice
|
||||||
|
: _cart.totalAmountWithDiscount + product.price,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_cart = _cart.copyWith(
|
||||||
|
products: [...existingProducts, product.copyWith(selectedAmount: 1)],
|
||||||
|
totalAmount: _cart.totalAmount + product.price,
|
||||||
|
totalAmountWithDiscount: product.isDiscounted
|
||||||
|
? _cart.totalAmountWithDiscount + product.discountPrice
|
||||||
|
: _cart.totalAmountWithDiscount + product.price,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_shoppingCartController.add(_cart);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<int> getCartLength() {
|
||||||
|
return _shoppingCartController.stream.map((cart) => cart.products.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<ShoppingCart> getShoppingCart() {
|
||||||
|
return _shoppingCartController.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> removeProductFromCart(Product product) {
|
||||||
|
var existingProducts = _cart.products;
|
||||||
|
if (existingProducts.contains(product)) {
|
||||||
|
var index = existingProducts.indexOf(product);
|
||||||
|
if (product.selectedAmount == 1) {
|
||||||
|
existingProducts.removeAt(index);
|
||||||
|
} else {
|
||||||
|
existingProducts[index] = product.copyWith(
|
||||||
|
selectedAmount: existingProducts[index].selectedAmount - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_cart = _cart.copyWith(
|
||||||
|
products: existingProducts,
|
||||||
|
totalAmount: _cart.totalAmount - product.price,
|
||||||
|
totalAmountWithDiscount: product.isDiscounted
|
||||||
|
? _cart.totalAmountWithDiscount - product.discountPrice
|
||||||
|
: _cart.totalAmountWithDiscount - product.price,
|
||||||
|
);
|
||||||
|
|
||||||
|
_shoppingCartController.add(_cart);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
}
|
25
packages/firebase_shopping_repository/pubspec.yaml
Normal file
25
packages/firebase_shopping_repository/pubspec.yaml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
name: firebase_shopping_repository
|
||||||
|
description: "A new Flutter package project."
|
||||||
|
version: 3.0.0
|
||||||
|
homepage: https://github.com/Iconica-Development/flutter_shopping/packages/firebase_shopping_repository
|
||||||
|
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.5.3
|
||||||
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
collection: ^1.18.0
|
||||||
|
rxdart: ^0.28.0
|
||||||
|
firebase_core: ^3.6.0
|
||||||
|
cloud_firestore: ^5.4.4
|
||||||
|
|
||||||
|
shopping_repository_interface:
|
||||||
|
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
|
||||||
|
version: ^3.0.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
|
@ -1,36 +0,0 @@
|
||||||
# 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>
|
|
|
@ -1,9 +0,0 @@
|
||||||
/// Flutter component for shopping cart.
|
|
||||||
library flutter_order_details;
|
|
||||||
|
|
||||||
export "package:flutter_form_wizard/flutter_form.dart";
|
|
||||||
|
|
||||||
export "src/configuration/order_detail_configuration.dart";
|
|
||||||
export "src/configuration/order_detail_translations.dart";
|
|
||||||
export "src/order_detail_screen.dart";
|
|
||||||
export "src/widgets/order_succes.dart";
|
|
|
@ -1,71 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_order_details/flutter_order_details.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Configuration for the order detail screen.
|
|
||||||
class OrderDetailConfiguration {
|
|
||||||
/// Constructor for the order detail configuration.
|
|
||||||
const OrderDetailConfiguration({
|
|
||||||
required this.shoppingService,
|
|
||||||
required this.onNextStep,
|
|
||||||
required this.onStepsCompleted,
|
|
||||||
required this.onCompleteOrderDetails,
|
|
||||||
this.pages,
|
|
||||||
this.translations = const OrderDetailTranslations(),
|
|
||||||
this.appBarBuilder,
|
|
||||||
this.nextbuttonBuilder,
|
|
||||||
this.orderSuccessBuilder,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The shopping service that is used
|
|
||||||
final ShoppingService shoppingService;
|
|
||||||
|
|
||||||
/// 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<FlutterFormPage> Function(BuildContext context)? pages;
|
|
||||||
|
|
||||||
/// 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(
|
|
||||||
String shopId,
|
|
||||||
List<Product> products,
|
|
||||||
Map<int, Map<String, dynamic>> value,
|
|
||||||
OrderDetailConfiguration configuration,
|
|
||||||
) onStepsCompleted;
|
|
||||||
|
|
||||||
/// Callback function that is called when the user has completed a step.
|
|
||||||
final Function(
|
|
||||||
int currentStep,
|
|
||||||
Map<String, dynamic> data,
|
|
||||||
FlutterFormController controller,
|
|
||||||
) onNextStep;
|
|
||||||
|
|
||||||
/// Localization for the order detail screen.
|
|
||||||
final OrderDetailTranslations translations;
|
|
||||||
|
|
||||||
/// Optional app bar that you can pass to the order detail screen.
|
|
||||||
final PreferredSizeWidget? Function(BuildContext context, String title)?
|
|
||||||
appBarBuilder;
|
|
||||||
|
|
||||||
/// Optional next button builder that you can pass to the order detail screen.
|
|
||||||
final Widget Function(
|
|
||||||
int currentStep,
|
|
||||||
// ignore: avoid_positional_boolean_parameters
|
|
||||||
bool checkingPages,
|
|
||||||
BuildContext context,
|
|
||||||
OrderDetailConfiguration configuration,
|
|
||||||
FlutterFormController controller,
|
|
||||||
)? nextbuttonBuilder;
|
|
||||||
|
|
||||||
/// Optional builder for the order success screen.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
OrderDetailConfiguration,
|
|
||||||
Map<int, Map<String, dynamic>> orderDetails,
|
|
||||||
)? orderSuccessBuilder;
|
|
||||||
|
|
||||||
/// This function is called after the order has been completed and
|
|
||||||
/// the success screen has been shown.
|
|
||||||
final Function(BuildContext context, OrderDetailConfiguration configuration)
|
|
||||||
onCompleteOrderDetails;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
/// Localizations for the order detail page.
|
|
||||||
class OrderDetailTranslations {
|
|
||||||
/// Constructor for the order detail localization.
|
|
||||||
const OrderDetailTranslations({
|
|
||||||
this.nextButton = "Order",
|
|
||||||
this.completeButton = "Complete",
|
|
||||||
this.orderDetailsTitle = "Information",
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Next button localization.
|
|
||||||
final String nextButton;
|
|
||||||
|
|
||||||
/// Complete button localization.
|
|
||||||
final String completeButton;
|
|
||||||
|
|
||||||
/// Title for the order details page.
|
|
||||||
final String orderDetailsTitle;
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_order_details/flutter_order_details.dart";
|
|
||||||
import "package:flutter_order_details/src/widgets/default_appbar.dart";
|
|
||||||
import "package:flutter_order_details/src/widgets/default_next_button.dart";
|
|
||||||
import "package:flutter_order_details/src/widgets/default_order_detail_pages.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> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var controller = FlutterFormController();
|
|
||||||
return Scaffold(
|
|
||||||
appBar: widget.configuration.appBarBuilder?.call(
|
|
||||||
context,
|
|
||||||
widget.configuration.translations.orderDetailsTitle,
|
|
||||||
) ??
|
|
||||||
DefaultAppbar(
|
|
||||||
title: widget.configuration.translations.orderDetailsTitle,
|
|
||||||
),
|
|
||||||
body: FlutterForm(
|
|
||||||
formController: controller,
|
|
||||||
options: FlutterFormOptions(
|
|
||||||
nextButton: (pageNumber, checkingPages) =>
|
|
||||||
widget.configuration.nextbuttonBuilder?.call(
|
|
||||||
pageNumber,
|
|
||||||
checkingPages,
|
|
||||||
context,
|
|
||||||
widget.configuration,
|
|
||||||
controller,
|
|
||||||
) ??
|
|
||||||
DefaultNextButton(
|
|
||||||
controller: controller,
|
|
||||||
configuration: widget.configuration,
|
|
||||||
currentStep: pageNumber,
|
|
||||||
checkingPages: checkingPages,
|
|
||||||
),
|
|
||||||
pages: widget.configuration.pages?.call(context) ??
|
|
||||||
defaultPages(context, () {
|
|
||||||
setState(() {});
|
|
||||||
}),
|
|
||||||
onFinished: (data) async {
|
|
||||||
widget.configuration.onStepsCompleted.call(
|
|
||||||
widget.configuration.shoppingService.shopService.selectedShop!.id,
|
|
||||||
widget.configuration.shoppingService.shoppingCartService.products,
|
|
||||||
data,
|
|
||||||
widget.configuration,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onNext: (step, data) {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
|
|
||||||
/// Default appbar for the order details page.
|
|
||||||
class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget {
|
|
||||||
/// Constructor for the default appbar for the order details page.
|
|
||||||
const DefaultAppbar({
|
|
||||||
required this.title,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Title of the appbar.
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return AppBar(
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
style: theme.textTheme.headlineLarge,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_order_details/flutter_order_details.dart";
|
|
||||||
|
|
||||||
/// Default next button for the order details page.
|
|
||||||
class DefaultNextButton extends StatelessWidget {
|
|
||||||
/// Constructor for the default next button for the order details page.
|
|
||||||
const DefaultNextButton({
|
|
||||||
required this.controller,
|
|
||||||
required this.configuration,
|
|
||||||
required this.currentStep,
|
|
||||||
required this.checkingPages,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configuration for the order details page.
|
|
||||||
final OrderDetailConfiguration configuration;
|
|
||||||
|
|
||||||
/// Controller for the form.
|
|
||||||
final FlutterFormController controller;
|
|
||||||
|
|
||||||
/// Current step in the form.
|
|
||||||
final int currentStep;
|
|
||||||
|
|
||||||
/// Whether the form is checking pages.
|
|
||||||
final bool checkingPages;
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
var nextButtonTexts = [
|
|
||||||
"Choose date and time",
|
|
||||||
"Next",
|
|
||||||
"Next",
|
|
||||||
];
|
|
||||||
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 32),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: () async {
|
|
||||||
configuration.onNextStep(
|
|
||||||
currentStep,
|
|
||||||
controller.getCurrentStepResults(),
|
|
||||||
controller,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
style: theme.filledButtonTheme.style?.copyWith(
|
|
||||||
backgroundColor: WidgetStateProperty.all(
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
nextButtonTexts[currentStep],
|
|
||||||
style: theme.textTheme.displayLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,431 +0,0 @@
|
||||||
import "package:animated_toggle/animated_toggle.dart";
|
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_order_details/flutter_order_details.dart";
|
|
||||||
|
|
||||||
/// Default pages for the order details screen.
|
|
||||||
List<FlutterFormPage> defaultPages(
|
|
||||||
BuildContext context,
|
|
||||||
Function() onSwitched,
|
|
||||||
) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
var morningTimes = <String>[
|
|
||||||
"09:00",
|
|
||||||
"09:15",
|
|
||||||
"09:30",
|
|
||||||
"09:45",
|
|
||||||
"10:00",
|
|
||||||
"10:15",
|
|
||||||
"10:30",
|
|
||||||
"10:45",
|
|
||||||
"11:00",
|
|
||||||
"11:15",
|
|
||||||
"11:30",
|
|
||||||
"11:45",
|
|
||||||
];
|
|
||||||
|
|
||||||
var afternoonTimes = <String>[
|
|
||||||
"12:00",
|
|
||||||
"12:15",
|
|
||||||
"12:30",
|
|
||||||
"12:45",
|
|
||||||
"13:00",
|
|
||||||
"13:15",
|
|
||||||
"13:30",
|
|
||||||
"13:45",
|
|
||||||
"14:00",
|
|
||||||
"14:15",
|
|
||||||
"14:30",
|
|
||||||
"14:45",
|
|
||||||
"15:00",
|
|
||||||
"15:15",
|
|
||||||
"15:30",
|
|
||||||
"15:45",
|
|
||||||
"16:00",
|
|
||||||
"16:15",
|
|
||||||
"16:30",
|
|
||||||
"16:45",
|
|
||||||
"17:00",
|
|
||||||
];
|
|
||||||
|
|
||||||
InputDecoration inputDecoration(String hint) => InputDecoration(
|
|
||||||
hintStyle: theme.textTheme.bodySmall,
|
|
||||||
hintText: hint,
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.white,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
InputDecoration dropdownInputDecoration(String hint) => InputDecoration(
|
|
||||||
hintStyle: theme.textTheme.bodySmall,
|
|
||||||
hintText: hint,
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.white,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
var switchStatus = ValueNotifier<bool>(false);
|
|
||||||
var multipleChoiceController = FlutterFormInputMultipleChoiceController(
|
|
||||||
id: "multipleChoice",
|
|
||||||
mandatory: true,
|
|
||||||
);
|
|
||||||
return [
|
|
||||||
FlutterFormPage(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"What's your name?",
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
FlutterFormInputPlainText(
|
|
||||||
decoration: inputDecoration("Name"),
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
controller: FlutterFormInputPlainTextController(
|
|
||||||
id: "name",
|
|
||||||
mandatory: true,
|
|
||||||
),
|
|
||||||
validationMessage: "Please enter your name",
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"What's your address?",
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
FlutterFormInputPlainText(
|
|
||||||
decoration: inputDecoration("Street and number"),
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
controller: FlutterFormInputPlainTextController(
|
|
||||||
id: "street",
|
|
||||||
mandatory: true,
|
|
||||||
),
|
|
||||||
validationMessage: "Please enter your address",
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return "Please enter a street and house number";
|
|
||||||
}
|
|
||||||
var regex = RegExp(r"^[A-Za-z]+\s[0-9]{1,3}$");
|
|
||||||
if (!regex.hasMatch(value)) {
|
|
||||||
return "Invalid street and house number";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
FlutterFormInputPlainText(
|
|
||||||
decoration: inputDecoration("Postal code"),
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
controller: FlutterFormInputPlainTextController(
|
|
||||||
id: "postalCode",
|
|
||||||
mandatory: true,
|
|
||||||
),
|
|
||||||
validationMessage: "Please enter your postal code",
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return "Please enter a postal code";
|
|
||||||
}
|
|
||||||
var regex = RegExp(r"^[0-9]{4}[A-Za-z]{2}$");
|
|
||||||
if (!regex.hasMatch(value)) {
|
|
||||||
return "Invalid postal code format";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
FlutterFormInputPlainText(
|
|
||||||
decoration: inputDecoration("City"),
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
controller: FlutterFormInputPlainTextController(
|
|
||||||
id: "city",
|
|
||||||
mandatory: true,
|
|
||||||
),
|
|
||||||
validationMessage: "Please enter your city",
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"What's your phone number?",
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
FlutterFormInputPhone(
|
|
||||||
numberFieldStyle: theme.textTheme.bodySmall,
|
|
||||||
textAlignVertical: TextAlignVertical.center,
|
|
||||||
decoration: inputDecoration("Phone number"),
|
|
||||||
controller: FlutterFormInputPhoneController(
|
|
||||||
id: "phone",
|
|
||||||
mandatory: true,
|
|
||||||
),
|
|
||||||
validationMessage: "Please enter your phone number",
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.number!.isEmpty) {
|
|
||||||
return "Please enter a phone number";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any spaces or hyphens from the input
|
|
||||||
var phoneNumber =
|
|
||||||
value.number!.replaceAll(RegExp(r"\s+|-"), "");
|
|
||||||
|
|
||||||
// Check the length of the remaining digits
|
|
||||||
if (phoneNumber.length != 10 && phoneNumber.length != 11) {
|
|
||||||
return "Invalid phone number length";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all remaining characters are digits
|
|
||||||
if (!phoneNumber.substring(1).contains(RegExp(r"^[0-9]*$"))) {
|
|
||||||
return "Phone number can only contain digits";
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all checks pass, return null (no error)
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"What's your email address?",
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
FlutterFormInputEmail(
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
decoration: inputDecoration("email address"),
|
|
||||||
controller: FlutterFormInputEmailController(
|
|
||||||
id: "email",
|
|
||||||
mandatory: true,
|
|
||||||
),
|
|
||||||
validationMessage: "Please fill in a valid email address",
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"Do you have any comments?",
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
FlutterFormInputPlainText(
|
|
||||||
decoration: inputDecoration("Optional"),
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
controller: FlutterFormInputPlainTextController(
|
|
||||||
id: "comments",
|
|
||||||
),
|
|
||||||
validationMessage: "Please enter your email address",
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 100,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FlutterFormPage(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
child: Text(
|
|
||||||
"When and at what time would you like to pick up your order?",
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
FlutterFormInputDropdown(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.keyboard_arrow_down,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
isDense: true,
|
|
||||||
decoration: dropdownInputDecoration("Select a day"),
|
|
||||||
validationMessage: "Please select a day",
|
|
||||||
controller: FlutterFormInputDropdownController(
|
|
||||||
id: "date",
|
|
||||||
mandatory: true,
|
|
||||||
),
|
|
||||||
items: [
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: "Today",
|
|
||||||
child: Text(
|
|
||||||
"Today",
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: "Tomorrow",
|
|
||||||
child: Text(
|
|
||||||
"Tomorrow",
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
child: AnimatedToggle(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
blurRadius: 5,
|
|
||||||
color: theme.colorScheme.primary.withOpacity(0.8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(50),
|
|
||||||
),
|
|
||||||
width: 280,
|
|
||||||
toggleColor: theme.colorScheme.primary,
|
|
||||||
onSwitch: (value) {
|
|
||||||
switchStatus.value = value;
|
|
||||||
onSwitched();
|
|
||||||
},
|
|
||||||
childLeft: Center(
|
|
||||||
child: Text(
|
|
||||||
"Morning",
|
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
|
||||||
color: switchStatus.value
|
|
||||||
? theme.colorScheme.primary
|
|
||||||
: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
childRight: Center(
|
|
||||||
child: Text(
|
|
||||||
"Afternoon",
|
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
|
||||||
color: switchStatus.value
|
|
||||||
? Colors.white
|
|
||||||
: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
FlutterFormInputMultipleChoice(
|
|
||||||
validationMessage: "Select a Time",
|
|
||||||
controller: multipleChoiceController,
|
|
||||||
options: switchStatus.value ? afternoonTimes : morningTimes,
|
|
||||||
mainAxisSpacing: 5,
|
|
||||||
crossAxisSpacing: 5,
|
|
||||||
childAspectRatio: 2,
|
|
||||||
height: MediaQuery.of(context).size.height * 0.6,
|
|
||||||
builder: (context, index, selected, controller, options, state) =>
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
state.didChange(options[index]);
|
|
||||||
selected.value = index;
|
|
||||||
controller.onSaved(options[index]);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: selected.value == index
|
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
height: 40,
|
|
||||||
child: Center(
|
|
||||||
child: Text(options[index]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FlutterFormPage(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Payment method",
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"Choose when you would like to to pay for the order.",
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 84,
|
|
||||||
),
|
|
||||||
FlutterFormInputMultipleChoice(
|
|
||||||
crossAxisCount: 1,
|
|
||||||
mainAxisSpacing: 24,
|
|
||||||
crossAxisSpacing: 5,
|
|
||||||
childAspectRatio: 2,
|
|
||||||
height: 420,
|
|
||||||
controller: FlutterFormInputMultipleChoiceController(
|
|
||||||
id: "payment",
|
|
||||||
mandatory: true,
|
|
||||||
),
|
|
||||||
options: const ["PAY NOW", "PAY AT THE CASHIER"],
|
|
||||||
builder: (context, index, selected, controller, options, state) =>
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
state.didChange(options[index]);
|
|
||||||
selected.value = index;
|
|
||||||
controller.onSaved(options[index]);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: selected.value == index
|
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
height: 40,
|
|
||||||
child: Center(child: Text(options[index])),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
validationMessage: "Please select a payment method",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,222 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_order_details/flutter_order_details.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Default order success widget.
|
|
||||||
class DefaultOrderSucces extends StatelessWidget {
|
|
||||||
/// Constructor for the DefaultOrderSucces.
|
|
||||||
const DefaultOrderSucces({
|
|
||||||
required this.configuration,
|
|
||||||
required this.orderDetails,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configuration for the user-stor
|
|
||||||
final OrderDetailConfiguration configuration;
|
|
||||||
|
|
||||||
/// Order details.
|
|
||||||
final Map<int, Map<String, dynamic>> orderDetails;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
var discountedProducts = configuration
|
|
||||||
.shoppingService.productService.products
|
|
||||||
.where((product) => product.hasDiscount)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(
|
|
||||||
"Confirmation",
|
|
||||||
style: theme.textTheme.headlineLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 32,
|
|
||||||
top: 32,
|
|
||||||
right: 32,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Success!",
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"Thank you ${orderDetails[0]!['name']} for your order!",
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"The order was placed"
|
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
" at ${configuration.shoppingService.shopService.selectedShop?.name}."
|
|
||||||
" You can pick this"
|
|
||||||
" up ${orderDetails[1]!['date']} at"
|
|
||||||
" ${orderDetails[1]!['multipleChoice']}.",
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"If you want, you can place another order in this street.",
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 32,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"Weekly offers",
|
|
||||||
style: theme.textTheme.headlineSmall
|
|
||||||
?.copyWith(color: Colors.black),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 272,
|
|
||||||
child: ListView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
children: [
|
|
||||||
const SizedBox(width: 32),
|
|
||||||
// _discount(context),
|
|
||||||
// const SizedBox(width: 8),
|
|
||||||
// _discount(context),
|
|
||||||
for (var product in discountedProducts) ...[
|
|
||||||
_discount(
|
|
||||||
context,
|
|
||||||
product,
|
|
||||||
configuration.shoppingService.shopService.selectedShop!,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(width: 32),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 60),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: () async {
|
|
||||||
configuration.onCompleteOrderDetails
|
|
||||||
.call(context, configuration);
|
|
||||||
},
|
|
||||||
style: theme.filledButtonTheme.style?.copyWith(
|
|
||||||
backgroundColor: WidgetStateProperty.all(
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"Place another order",
|
|
||||||
style: theme.textTheme.displayLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _discount(BuildContext context, Product product, Shop shop) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
width: MediaQuery.of(context).size.width - 64,
|
|
||||||
height: 200,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
child: Image.network(
|
|
||||||
product.imageUrl,
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
height: 38,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.black,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(10),
|
|
||||||
topRight: Radius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
|
||||||
child: Text(
|
|
||||||
shop.name,
|
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: Container(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
bottomLeft: Radius.circular(10),
|
|
||||||
bottomRight: Radius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
height: 68,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
|
||||||
child: Text(
|
|
||||||
"${product.name}, now for ${product.price.toStringAsFixed(2)}",
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
name: flutter_order_details
|
|
||||||
description: "A Flutter module for order details."
|
|
||||||
version: 2.0.0
|
|
||||||
publish_to: "none"
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
animated_toggle:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_animated_toggle
|
|
||||||
ref: 0.0.3
|
|
||||||
flutter_form_wizard:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_form_wizard
|
|
||||||
ref: 6.5.0
|
|
||||||
flutter_shopping_interface:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
|
||||||
path: packages/flutter_shopping_interface
|
|
||||||
ref: 2.0.0
|
|
||||||
collection: ^1.18.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_test:
|
|
||||||
sdk: flutter
|
|
||||||
flutter_iconica_analysis:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
|
||||||
ref: 7.0.0
|
|
||||||
|
|
||||||
flutter:
|
|
|
@ -1,47 +0,0 @@
|
||||||
# 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>
|
|
|
@ -1,9 +0,0 @@
|
||||||
include: package:flutter_iconica_analysis/components_options.yaml
|
|
||||||
|
|
||||||
# Possible to overwrite the rules from the package
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
|
@ -1,8 +0,0 @@
|
||||||
/// 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_configuration.dart";
|
|
||||||
export "src/configuration/product_page_shop_selector_style.dart";
|
|
||||||
export "src/configuration/product_page_translations.dart";
|
|
||||||
export "src/product_page_screen.dart";
|
|
|
@ -1,67 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_product_page/flutter_product_page.dart";
|
|
||||||
|
|
||||||
/// Category selection screen.
|
|
||||||
class CategorySelectionScreen extends StatelessWidget {
|
|
||||||
/// Constructor for the category selection screen.
|
|
||||||
const CategorySelectionScreen({
|
|
||||||
required this.configuration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configuration for the product page.
|
|
||||||
final ProductPageConfiguration configuration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: const SizedBox.shrink(),
|
|
||||||
title: Text(
|
|
||||||
"filter",
|
|
||||||
style: theme.textTheme.headlineLarge,
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: ListenableBuilder(
|
|
||||||
listenable: configuration.shoppingService.productService,
|
|
||||||
builder: (context, _) => Column(
|
|
||||||
children: [
|
|
||||||
...configuration.shoppingService.productService.getCategories().map(
|
|
||||||
(category) {
|
|
||||||
var isChecked = configuration
|
|
||||||
.shoppingService.productService.selectedCategories
|
|
||||||
.contains(category);
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: CheckboxListTile(
|
|
||||||
activeColor: theme.colorScheme.primary,
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
value: isChecked,
|
|
||||||
onChanged: (value) {
|
|
||||||
configuration.shoppingService.productService
|
|
||||||
.selectCategory(category);
|
|
||||||
},
|
|
||||||
shape: const UnderlineInputBorder(),
|
|
||||||
title: Text(
|
|
||||||
category,
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,173 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_product_page/flutter_product_page.dart";
|
|
||||||
import "package:flutter_product_page/src/widgets/product_item_popup.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Configuration for the product page.
|
|
||||||
class ProductPageConfiguration {
|
|
||||||
/// Constructor for the product page configuration.
|
|
||||||
ProductPageConfiguration({
|
|
||||||
required this.shoppingService,
|
|
||||||
required this.shops,
|
|
||||||
required this.getProducts,
|
|
||||||
required this.onAddToCart,
|
|
||||||
required this.onNavigateToShoppingCart,
|
|
||||||
required this.getProductsInShoppingCart,
|
|
||||||
this.shoppingCartButtonBuilder,
|
|
||||||
this.initialShopId,
|
|
||||||
this.productBuilder,
|
|
||||||
this.onShopSelectionChange,
|
|
||||||
this.translations = const ProductPageTranslations(),
|
|
||||||
this.shopSelectorStyle = ShopSelectorStyle.row,
|
|
||||||
this.pagePadding = const EdgeInsets.all(4),
|
|
||||||
this.appBarBuilder,
|
|
||||||
this.bottomNavigationBar,
|
|
||||||
this.onProductDetail,
|
|
||||||
this.discountDescription,
|
|
||||||
this.noContentBuilder,
|
|
||||||
this.errorBuilder,
|
|
||||||
this.shopselectorBuilder,
|
|
||||||
this.discountBuilder,
|
|
||||||
this.categoryListBuilder,
|
|
||||||
this.selectedCategoryBuilder,
|
|
||||||
}) {
|
|
||||||
onProductDetail ??= _onProductDetail;
|
|
||||||
discountDescription ??= _defaultDiscountDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The shopping service that is used
|
|
||||||
final ShoppingService shoppingService;
|
|
||||||
|
|
||||||
/// 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<Shop>> Function() shops;
|
|
||||||
|
|
||||||
/// A function that returns all the products that belong to a certain shop.
|
|
||||||
/// The function must return a [List<Product>].
|
|
||||||
final Future<List<Product>> Function(Shop shop) getProducts;
|
|
||||||
|
|
||||||
/// The localizations for the product page.
|
|
||||||
final ProductPageTranslations translations;
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
Product product,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
)? productBuilder;
|
|
||||||
|
|
||||||
/// The builder for the product popup. This builder should return a widget
|
|
||||||
Function(
|
|
||||||
BuildContext context,
|
|
||||||
Product product,
|
|
||||||
String closeText,
|
|
||||||
)? onProductDetail;
|
|
||||||
|
|
||||||
/// The builder for the shopping cart. This builder should return a widget
|
|
||||||
/// that navigates to the shopping cart overview page.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
)? shoppingCartButtonBuilder;
|
|
||||||
|
|
||||||
/// The function that returns the discount description for a product.
|
|
||||||
String Function(
|
|
||||||
Product product,
|
|
||||||
)? discountDescription;
|
|
||||||
|
|
||||||
/// This function must be implemented by the developer and should handle the
|
|
||||||
/// adding of a product to the cart.
|
|
||||||
Function(Product 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(Shop 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 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 Function(BuildContext context)? appBarBuilder;
|
|
||||||
|
|
||||||
/// Builder for the no content widget. This builder is used when there is no
|
|
||||||
/// content to display.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
)? noContentBuilder;
|
|
||||||
|
|
||||||
/// Builder for the error widget. This builder is used when there is an error
|
|
||||||
/// to display.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
Object? error,
|
|
||||||
)? errorBuilder;
|
|
||||||
|
|
||||||
/// Builder for the shop selector. This builder is used to build the shop
|
|
||||||
/// selector that will be displayed in the product page.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
List<Shop> shops,
|
|
||||||
Function(Shop shop) onShopSelectionChange,
|
|
||||||
)? shopselectorBuilder;
|
|
||||||
|
|
||||||
/// Builder for the discount widget. This builder is used to build the
|
|
||||||
/// discount widget that will be displayed in the product page.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
List<Product> discountedProducts,
|
|
||||||
)? discountBuilder;
|
|
||||||
|
|
||||||
/// Builder for the list of items that are displayed in the product page.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
List<Product> products,
|
|
||||||
)? categoryListBuilder;
|
|
||||||
|
|
||||||
/// Builder for the list of selected categories
|
|
||||||
final Widget Function(ProductPageConfiguration configuration)?
|
|
||||||
selectedCategoryBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onProductDetail(
|
|
||||||
BuildContext context,
|
|
||||||
Product product,
|
|
||||||
String closeText,
|
|
||||||
) async {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
await showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
backgroundColor: theme.colorScheme.surface,
|
|
||||||
builder: (context) => ProductItemPopup(
|
|
||||||
product: product,
|
|
||||||
closeText: closeText,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _defaultDiscountDescription(
|
|
||||||
Product product,
|
|
||||||
) =>
|
|
||||||
"${product.name}, now for ${product.discountPrice} each";
|
|
|
@ -1,8 +0,0 @@
|
||||||
/// 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,
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
/// Localization for the product page
|
|
||||||
class ProductPageTranslations {
|
|
||||||
/// Default constructor
|
|
||||||
const ProductPageTranslations({
|
|
||||||
this.navigateToShoppingCart = "View shopping cart",
|
|
||||||
this.discountTitle = "Weekly offer",
|
|
||||||
this.failedToLoadImageExplenation = "Failed to load image",
|
|
||||||
this.close = "Close",
|
|
||||||
this.categoryItemListTitle = "What would you like to order",
|
|
||||||
this.appBarTitle = "ProductPage",
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
|
|
||||||
/// Title for the category item list
|
|
||||||
final String categoryItemListTitle;
|
|
||||||
|
|
||||||
/// Title for the app bar
|
|
||||||
final String appBarTitle;
|
|
||||||
}
|
|
|
@ -1,306 +0,0 @@
|
||||||
import "package:collection/collection.dart";
|
|
||||||
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/widgets/defaults/default_appbar.dart";
|
|
||||||
import "package:flutter_product_page/src/widgets/defaults/default_error.dart";
|
|
||||||
import "package:flutter_product_page/src/widgets/defaults/default_no_content.dart";
|
|
||||||
import "package:flutter_product_page/src/widgets/defaults/default_shopping_cart_button.dart";
|
|
||||||
import "package:flutter_product_page/src/widgets/defaults/selected_categories.dart";
|
|
||||||
import "package:flutter_product_page/src/widgets/shop_selector.dart";
|
|
||||||
import "package:flutter_product_page/src/widgets/weekly_discount.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// A page that displays products.
|
|
||||||
class ProductPageScreen extends StatefulWidget {
|
|
||||||
/// Constructor for the product page.
|
|
||||||
const ProductPageScreen({
|
|
||||||
required this.configuration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configuration for the product page.
|
|
||||||
final ProductPageConfiguration configuration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ProductPageScreen> createState() => _ProductPageScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ProductPageScreenState extends State<ProductPageScreen> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Scaffold(
|
|
||||||
appBar: widget.configuration.appBarBuilder?.call(context) ??
|
|
||||||
DefaultAppbar(
|
|
||||||
configuration: widget.configuration,
|
|
||||||
),
|
|
||||||
bottomNavigationBar: widget.configuration.bottomNavigationBar,
|
|
||||||
body: SafeArea(
|
|
||||||
child: Padding(
|
|
||||||
padding: widget.configuration.pagePadding,
|
|
||||||
child: FutureBuilder(
|
|
||||||
// ignore: discarded_futures
|
|
||||||
future: widget.configuration.shops(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
List<Shop>? shops;
|
|
||||||
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return const Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Center(child: CircularProgressIndicator.adaptive()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
return widget.configuration.errorBuilder?.call(
|
|
||||||
context,
|
|
||||||
snapshot.error,
|
|
||||||
) ??
|
|
||||||
DefaultError(
|
|
||||||
error: snapshot.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
shops = snapshot.data;
|
|
||||||
|
|
||||||
if (shops == null || shops.isEmpty) {
|
|
||||||
return widget.configuration.errorBuilder?.call(
|
|
||||||
context,
|
|
||||||
snapshot.error,
|
|
||||||
) ??
|
|
||||||
DefaultError(error: snapshot.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.configuration.initialShopId != null) {
|
|
||||||
var initialShop = shops.firstWhereOrNull(
|
|
||||||
(shop) => shop.id == widget.configuration.initialShopId,
|
|
||||||
);
|
|
||||||
if (initialShop != null) {
|
|
||||||
widget.configuration.shoppingService.shopService.selectShop(
|
|
||||||
initialShop,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
widget.configuration.shoppingService.shopService.selectShop(
|
|
||||||
shops.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
widget.configuration.shoppingService.shopService.selectShop(
|
|
||||||
shops.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return _ProductPageContent(
|
|
||||||
configuration: widget.configuration,
|
|
||||||
shops: shops,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ProductPageContent extends StatefulWidget {
|
|
||||||
const _ProductPageContent({
|
|
||||||
required this.configuration,
|
|
||||||
required this.shops,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ProductPageConfiguration configuration;
|
|
||||||
|
|
||||||
final List<Shop> shops;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_ProductPageContent> createState() => _ProductPageContentState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ProductPageContentState extends State<_ProductPageContent> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Stack(
|
|
||||||
children: [
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// shop selector
|
|
||||||
widget.configuration.shopselectorBuilder?.call(
|
|
||||||
context,
|
|
||||||
widget.configuration,
|
|
||||||
widget.shops,
|
|
||||||
widget
|
|
||||||
.configuration.shoppingService.shopService.selectShop,
|
|
||||||
) ??
|
|
||||||
ShopSelector(
|
|
||||||
configuration: widget.configuration,
|
|
||||||
shops: widget.shops,
|
|
||||||
onTap: (shop) {
|
|
||||||
widget.configuration.shoppingService.shopService
|
|
||||||
.selectShop(shop);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// selected categories
|
|
||||||
widget.configuration.selectedCategoryBuilder?.call(
|
|
||||||
widget.configuration,
|
|
||||||
) ??
|
|
||||||
SelectedCategories(
|
|
||||||
configuration: widget.configuration,
|
|
||||||
),
|
|
||||||
// products
|
|
||||||
_ShopContents(
|
|
||||||
configuration: widget.configuration,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// button
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: widget.configuration.shoppingCartButtonBuilder != null
|
|
||||||
? widget.configuration.shoppingCartButtonBuilder!(
|
|
||||||
context,
|
|
||||||
widget.configuration,
|
|
||||||
)
|
|
||||||
: DefaultShoppingCartButton(
|
|
||||||
configuration: widget.configuration,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ShopContents extends StatefulWidget {
|
|
||||||
const _ShopContents({
|
|
||||||
required this.configuration,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ProductPageConfiguration configuration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_ShopContents> createState() => _ShopContentsState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ShopContentsState extends State<_ShopContents> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
widget.configuration.shoppingService.shopService.addListener(_listen);
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.configuration.shoppingService.shopService.removeListener(_listen);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listen() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: widget.configuration.pagePadding.horizontal,
|
|
||||||
),
|
|
||||||
child: FutureBuilder(
|
|
||||||
// ignore: discarded_futures
|
|
||||||
future: widget.configuration.getProducts(
|
|
||||||
widget.configuration.shoppingService.shopService.selectedShop!,
|
|
||||||
),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
List<Product> productPageContent;
|
|
||||||
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return SizedBox(
|
|
||||||
height: MediaQuery.of(context).size.height * 0.7,
|
|
||||||
child: const Center(
|
|
||||||
child: CircularProgressIndicator.adaptive(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
if (widget.configuration.errorBuilder != null) {
|
|
||||||
return widget.configuration.errorBuilder!(
|
|
||||||
context,
|
|
||||||
snapshot.error,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return DefaultError(error: snapshot.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
productPageContent =
|
|
||||||
widget.configuration.shoppingService.productService.products;
|
|
||||||
|
|
||||||
if (productPageContent.isEmpty) {
|
|
||||||
return widget.configuration.noContentBuilder?.call(context) ??
|
|
||||||
const DefaultNoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
var discountedproducts = productPageContent
|
|
||||||
.where((product) => product.hasDiscount)
|
|
||||||
.toList();
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Discounted product
|
|
||||||
if (discountedproducts.isNotEmpty) ...[
|
|
||||||
widget.configuration.discountBuilder?.call(
|
|
||||||
context,
|
|
||||||
widget.configuration,
|
|
||||||
discountedproducts,
|
|
||||||
) ??
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: WeeklyDiscount(
|
|
||||||
configuration: widget.configuration,
|
|
||||||
product: discountedproducts.first,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
|
||||||
child: Text(
|
|
||||||
widget.configuration.translations.categoryItemListTitle,
|
|
||||||
style: theme.textTheme.titleLarge,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
widget.configuration.categoryListBuilder?.call(
|
|
||||||
context,
|
|
||||||
widget.configuration,
|
|
||||||
productPageContent,
|
|
||||||
) ??
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// Products
|
|
||||||
|
|
||||||
getCategoryList(
|
|
||||||
context,
|
|
||||||
widget.configuration,
|
|
||||||
widget.configuration.shoppingService.productService
|
|
||||||
.products,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Bottom padding so the last product is not cut off
|
|
||||||
// by the to shopping cart button.
|
|
||||||
const SizedBox(height: 48),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
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/widgets/defaults/default_product_item.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Generates a [CategoryList] from a list of [Product]s and a
|
|
||||||
/// [ProductPageConfiguration].
|
|
||||||
Widget getCategoryList(
|
|
||||||
BuildContext context,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
List<Product> products,
|
|
||||||
) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
var categorizedProducts = <String, List<Product>>{};
|
|
||||||
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
|
|
||||||
?.call(context, product, configuration) ??
|
|
||||||
DefaultProductItem(
|
|
||||||
product: product,
|
|
||||||
onAddToCart: configuration.onAddToCart,
|
|
||||||
onProductDetail: configuration.onProductDetail!,
|
|
||||||
translations: configuration.translations,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
var category = Category(
|
|
||||||
name: categoryName,
|
|
||||||
content: productWidgets,
|
|
||||||
);
|
|
||||||
categories.add(category);
|
|
||||||
});
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
for (var category in categories) ...[
|
|
||||||
Text(
|
|
||||||
category.name!,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: category.content,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_product_page/flutter_product_page.dart";
|
|
||||||
import "package:flutter_product_page/src/category_selection_screen.dart";
|
|
||||||
|
|
||||||
/// Default appbar for the product page.
|
|
||||||
class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget {
|
|
||||||
/// Constructor for the default appbar for the product page.
|
|
||||||
const DefaultAppbar({
|
|
||||||
required this.configuration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configuration for the product page.
|
|
||||||
final ProductPageConfiguration configuration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
return AppBar(
|
|
||||||
leading: IconButton(onPressed: () {}, icon: const Icon(Icons.person)),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => CategorySelectionScreen(
|
|
||||||
configuration: configuration,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.filter_alt),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
title: Text(
|
|
||||||
configuration.translations.appBarTitle,
|
|
||||||
style: theme.textTheme.headlineLarge,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
|
|
||||||
/// Default error widget.
|
|
||||||
class DefaultError extends StatelessWidget {
|
|
||||||
/// Constructor for the default error widget.
|
|
||||||
const DefaultError({
|
|
||||||
super.key,
|
|
||||||
this.error,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Error that occurred.
|
|
||||||
final Object? error;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
"Error: $error",
|
|
||||||
style: theme.textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
|
|
||||||
/// Default no content widget.
|
|
||||||
class DefaultNoContent extends StatelessWidget {
|
|
||||||
/// Constructor for the default no content widget.
|
|
||||||
const DefaultNoContent({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
"No content",
|
|
||||||
style: theme.textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,191 +0,0 @@
|
||||||
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:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
import "package:skeletonizer/skeletonizer.dart";
|
|
||||||
|
|
||||||
/// Product item widget.
|
|
||||||
class DefaultProductItem extends StatelessWidget {
|
|
||||||
/// Constructor for the product item widget.
|
|
||||||
const DefaultProductItem({
|
|
||||||
required this.product,
|
|
||||||
required this.onProductDetail,
|
|
||||||
required this.onAddToCart,
|
|
||||||
required this.translations,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Product to display.
|
|
||||||
final Product product;
|
|
||||||
|
|
||||||
/// Function to call when the product detail is requested.
|
|
||||||
final Function(
|
|
||||||
BuildContext context,
|
|
||||||
Product selectedProduct,
|
|
||||||
String closeText,
|
|
||||||
) onProductDetail;
|
|
||||||
|
|
||||||
/// Function to call when the product is added to the cart.
|
|
||||||
final Function(Product selectedProduct) onAddToCart;
|
|
||||||
|
|
||||||
/// Localizations for the product page.
|
|
||||||
final ProductPageTranslations translations;
|
|
||||||
|
|
||||||
/// 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: translations.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,
|
|
||||||
translations.close,
|
|
||||||
),
|
|
||||||
icon: Icon(
|
|
||||||
Icons.info_outline,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
var productInteraction = Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
_PriceLabel(
|
|
||||||
product: product,
|
|
||||||
),
|
|
||||||
_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.product,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Product product;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
if (product.hasDiscount) ...[
|
|
||||||
Text(
|
|
||||||
product.price.toStringAsFixed(2),
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
decoration: TextDecoration.lineThrough,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
],
|
|
||||||
Text(
|
|
||||||
product.hasDiscount
|
|
||||||
? product.discountPrice!.toStringAsFixed(2)
|
|
||||||
: product.price.toStringAsFixed(2),
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AddToCardButton extends StatelessWidget {
|
|
||||||
const _AddToCardButton({
|
|
||||||
required this.product,
|
|
||||||
required this.onAddToCart,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Product product;
|
|
||||||
final Function(Product product) onAddToCart;
|
|
||||||
|
|
||||||
static const double boxSize = 29;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
width: boxSize,
|
|
||||||
height: boxSize,
|
|
||||||
child: Center(
|
|
||||||
child: IconButton(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.add,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
onPressed: () => onAddToCart(product),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_product_page/flutter_product_page.dart";
|
|
||||||
|
|
||||||
/// Default shopping cart button for the product page.
|
|
||||||
class DefaultShoppingCartButton extends StatefulWidget {
|
|
||||||
/// Constructor for the default shopping cart button for the product page.
|
|
||||||
const DefaultShoppingCartButton({
|
|
||||||
required this.configuration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configuration for the product page.
|
|
||||||
final ProductPageConfiguration configuration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DefaultShoppingCartButton> createState() =>
|
|
||||||
_DefaultShoppingCartButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DefaultShoppingCartButtonState extends State<DefaultShoppingCartButton> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
widget.configuration.shoppingService.shoppingCartService
|
|
||||||
.addListener(_listen);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.configuration.shoppingService.shoppingCartService
|
|
||||||
.removeListener(_listen);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listen() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 60),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: widget.configuration.shoppingService.shoppingCartService
|
|
||||||
.products.isNotEmpty
|
|
||||||
? widget.configuration.onNavigateToShoppingCart
|
|
||||||
: null,
|
|
||||||
style: theme.filledButtonTheme.style?.copyWith(
|
|
||||||
backgroundColor: WidgetStateProperty.all(
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
widget.configuration.translations.navigateToShoppingCart,
|
|
||||||
style: theme.textTheme.displayLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_product_page/flutter_product_page.dart";
|
|
||||||
|
|
||||||
/// Selected categories.
|
|
||||||
class SelectedCategories extends StatefulWidget {
|
|
||||||
/// Constructor for the selected categories.
|
|
||||||
const SelectedCategories({
|
|
||||||
required this.configuration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configuration for the product page.
|
|
||||||
final ProductPageConfiguration configuration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SelectedCategories> createState() => _SelectedCategoriesState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SelectedCategoriesState extends State<SelectedCategories> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
widget.configuration.shoppingService.productService.addListener(_listen);
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.configuration.shoppingService.productService.removeListener(_listen);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listen() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 4),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
for (var category in widget.configuration.shoppingService
|
|
||||||
.productService.selectedCategories) ...[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8),
|
|
||||||
child: Chip(
|
|
||||||
backgroundColor: theme.colorScheme.primary,
|
|
||||||
deleteIcon: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
onDeleted: () {
|
|
||||||
widget.configuration.shoppingService.productService
|
|
||||||
.selectCategory(category);
|
|
||||||
},
|
|
||||||
label: Text(
|
|
||||||
category,
|
|
||||||
style: theme.textTheme.bodyMedium
|
|
||||||
?.copyWith(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.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 = 6,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// List of items.
|
|
||||||
final List<Shop> 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(Shop shop) onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 4,
|
|
||||||
),
|
|
||||||
child: 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
|
|
||||||
: Colors.white,
|
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.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.closeText,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The product to display.
|
|
||||||
final Product product;
|
|
||||||
|
|
||||||
/// Configuration for the product page.
|
|
||||||
final String closeText;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
product.description,
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 20, left: 40, right: 40),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
style: theme.filledButtonTheme.style?.copyWith(
|
|
||||||
backgroundColor: WidgetStateProperty.all(
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0,
|
|
||||||
vertical: 8.0,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
closeText,
|
|
||||||
style: theme.textTheme.displayLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_product_page/flutter_product_page.dart";
|
|
||||||
import "package:flutter_product_page/src/widgets/horizontal_list_items.dart";
|
|
||||||
import "package:flutter_product_page/src/widgets/spaced_wrap.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Shop selector widget that displays a list to navigate between shops.
|
|
||||||
class ShopSelector extends StatefulWidget {
|
|
||||||
/// Constructor for the shop selector.
|
|
||||||
const ShopSelector({
|
|
||||||
required this.configuration,
|
|
||||||
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.
|
|
||||||
|
|
||||||
/// List of shops.
|
|
||||||
final List<Shop> shops;
|
|
||||||
|
|
||||||
/// Callback when a shop is tapped.
|
|
||||||
final Function(Shop shop) onTap;
|
|
||||||
|
|
||||||
/// Padding between the buttons.
|
|
||||||
final double paddingBetweenButtons;
|
|
||||||
|
|
||||||
/// Padding on the buttons.
|
|
||||||
final double paddingOnButtons;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ShopSelector> createState() => _ShopSelectorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ShopSelectorState extends State<ShopSelector> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
widget.configuration.shoppingService.shopService.addListener(_listen);
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.configuration.shoppingService.shopService.removeListener(_listen);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listen() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (widget.shops.length == 1) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.configuration.shopSelectorStyle ==
|
|
||||||
ShopSelectorStyle.spacedWrap) {
|
|
||||||
return SpacedWrap(
|
|
||||||
shops: widget.shops,
|
|
||||||
selectedItem:
|
|
||||||
widget.configuration.shoppingService.shopService.selectedShop!.id,
|
|
||||||
onTap: widget.onTap,
|
|
||||||
width: MediaQuery.of(context).size.width - (16 * 2),
|
|
||||||
paddingBetweenButtons: widget.paddingBetweenButtons,
|
|
||||||
paddingOnButtons: widget.paddingOnButtons,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return HorizontalListItems(
|
|
||||||
shops: widget.shops,
|
|
||||||
selectedItem:
|
|
||||||
widget.configuration.shoppingService.shopService.selectedShop!.id,
|
|
||||||
onTap: widget.onTap,
|
|
||||||
paddingBetweenButtons: widget.paddingBetweenButtons,
|
|
||||||
paddingOnButtons: widget.paddingOnButtons,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.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<Shop> 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(Shop shop) onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return Wrap(
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 4,
|
|
||||||
children: [
|
|
||||||
for (var shop in shops) ...[
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(top: paddingBetweenButtons),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => onTap(shop),
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: shop.id == selectedItem
|
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
shop.name,
|
|
||||||
style: shop.id == selectedItem
|
|
||||||
? theme.textTheme.titleMedium
|
|
||||||
?.copyWith(color: Colors.white)
|
|
||||||
: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
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:flutter_shopping_interface/flutter_shopping_interface.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 Product product;
|
|
||||||
|
|
||||||
/// The top padding of the widget.
|
|
||||||
static const double topPadding = 20;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
var bottomText = Padding(
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
child: Text(
|
|
||||||
configuration.discountDescription!(product),
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
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.translations.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: const BoxDecoration(
|
|
||||||
color: Colors.black,
|
|
||||||
borderRadius: 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.translations.discountTitle,
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
var boxDecoration = BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
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,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
name: flutter_product_page
|
|
||||||
description: "A Flutter module for the product page"
|
|
||||||
publish_to: "none"
|
|
||||||
version: 2.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
|
|
||||||
flutter_shopping_interface:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
|
||||||
path: packages/flutter_shopping_interface
|
|
||||||
ref: 2.0.0
|
|
||||||
collection: ^1.18.0
|
|
||||||
provider: ^6.1.2
|
|
||||||
|
|
||||||
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
|
|
35
packages/flutter_shopping/.gitignore
vendored
35
packages/flutter_shopping/.gitignore
vendored
|
@ -9,7 +9,6 @@
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
migrate_working_dir/
|
migrate_working_dir/
|
||||||
.metadata
|
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
|
@ -20,37 +19,11 @@ migrate_working_dir/
|
||||||
# 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
|
||||||
# is commented out by default.
|
# is commented out by default.
|
||||||
.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
|
build/
|
||||||
.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/
|
|
||||||
|
|
|
@ -1,181 +1,39 @@
|
||||||
# flutter_shopping
|
<!--
|
||||||
|
This README describes the package. If you publish this package to pub.dev,
|
||||||
|
this README's contents appear on the landing page for your package.
|
||||||
|
|
||||||
The flutter_shopping user-story allows you to create an user shopping flow within minutes of work. This user-story contains the ability to show products, shopping cart, gathering user information and order succes and failed screens.
|
For information about how to write a good package README, see the guide for
|
||||||
|
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
|
||||||
|
|
||||||
## Setup
|
For general information about developing packages, see the Dart guide for
|
||||||
|
[creating packages](https://dart.dev/guides/libraries/create-packages)
|
||||||
|
and the Flutter guide for
|
||||||
|
[developing packages and plugins](https://flutter.dev/to/develop-packages).
|
||||||
|
-->
|
||||||
|
|
||||||
(1) Set up your `MyShop` model by extending from the `ProductPageShop` class. The most basic version looks like this:
|
TODO: Put a short description of the package here that helps potential users
|
||||||
|
know whether this package might be useful for them.
|
||||||
|
|
||||||
```dart
|
## Features
|
||||||
class MyShop extends ProductPageShop {
|
|
||||||
const MyShop({
|
|
||||||
required super.id,
|
|
||||||
required super.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
(2) Set up your `MyProduct` model by extending from `ShoppingCartProduct` and extending from the mixin `ProductPageProduct`, like this:
|
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||||
|
|
||||||
```dart
|
## Getting started
|
||||||
class MyProduct extends ShoppingCartProduct with ProductPageProduct {
|
|
||||||
MyProduct({
|
|
||||||
required super.id,
|
|
||||||
required super.name,
|
|
||||||
required super.price,
|
|
||||||
required this.category,
|
|
||||||
required this.imageUrl,
|
|
||||||
this.discountPrice,
|
|
||||||
this.hasDiscount = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
TODO: List prerequisites and provide or point to information on how to
|
||||||
final String category;
|
start using the package.
|
||||||
|
|
||||||
@override
|
|
||||||
final String imageUrl;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final double? discountPrice;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final bool hasDiscount;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
(3) Finally in your `routes.dart` import all the routes from the user-story:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
...getShoppingStoryRoutes(
|
|
||||||
configuration: ...
|
|
||||||
),
|
|
||||||
```
|
|
||||||
|
|
||||||
(4) Create a new instantiation of the ProductService class:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final ProductService<MyProduct> productService = ProductService([]);
|
|
||||||
```
|
|
||||||
|
|
||||||
(5) Set up the `FlutterShoppingConfiguration`:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
FlutterShoppingConfiguration(
|
|
||||||
// (REQUIRED): Shop builder configuration
|
|
||||||
shopBuilder: (BuildContext context) => ProductPageScreen(
|
|
||||||
configuration: ProductPageConfiguration(
|
|
||||||
// (REQUIRED): List of shops that should be displayed
|
|
||||||
// If there is only one, make a list with just one shop.
|
|
||||||
shops: getShops(),
|
|
||||||
|
|
||||||
// (REQUIRED): Function to add a product to the cart
|
|
||||||
onAddToCart: (ProductPageProduct product) =>
|
|
||||||
productService.addProduct(product as MyProduct),
|
|
||||||
|
|
||||||
// (REQUIRED): Function to get the products for a shop
|
|
||||||
getProducts: (String shopId) => Future<ProductPageContent>.value(
|
|
||||||
getShopContent(shopId),
|
|
||||||
),
|
|
||||||
|
|
||||||
// (REQUIRED): Function to navigate to the shopping cart
|
|
||||||
onNavigateToShoppingCart: () => onCompleteProductPage(context),
|
|
||||||
|
|
||||||
// (RECOMMENDED) Function that returns the description for a
|
|
||||||
// product that is on sale.
|
|
||||||
getDiscountDescription: (ProductPageProduct product) =>
|
|
||||||
"""${product.name} for just \$${product.discountPrice?.toStringAsFixed(2)}""",
|
|
||||||
|
|
||||||
// (RECOMMENDED) Function that is fired when the shop selection
|
|
||||||
// changes. You could use this to clear your shopping cart or to
|
|
||||||
// change the products so they belong to the correct shop again.
|
|
||||||
onShopSelectionChange: (ProductPageShop shop) => productService.clear(),
|
|
||||||
|
|
||||||
// (RECOMMENDED) The shop that is initially selected.
|
|
||||||
// Must be one of the shops in the [shops] list.
|
|
||||||
initialShop: getShops().first,
|
|
||||||
|
|
||||||
// (RECOMMENDED) Localizations for the product page.
|
|
||||||
localizations: const ProductPageLocalization(),
|
|
||||||
|
|
||||||
// (OPTIONAL) Appbar
|
|
||||||
appBar: ...
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// (REQUIRED): Shopping cart builder configuration
|
|
||||||
shoppingCartBuilder: (BuildContext context) => ShoppingCartScreen(
|
|
||||||
configuration: ShoppingCartConfig(
|
|
||||||
// (REQUIRED) product service instance:
|
|
||||||
productService: productService,
|
|
||||||
|
|
||||||
// (REQUIRED) product item builder:
|
|
||||||
productItemBuilder: (context, locale, product) => ...
|
|
||||||
|
|
||||||
// (OPTIONAL/REQUIRED) on confirm order callback:
|
|
||||||
// Either use this callback or the placeOrderButtonBuilder.
|
|
||||||
onConfirmOrder: (products) => onCompleteShoppingCart(context),
|
|
||||||
|
|
||||||
// (RECOMMENDED) localizations:
|
|
||||||
localizations: const ShoppingCartLocalizations(),
|
|
||||||
|
|
||||||
// (OPTIONAL) custom appbar:
|
|
||||||
appBar: ...
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// (REQUIRED): Configuration on what to do when the user story is
|
|
||||||
// completed.
|
|
||||||
onCompleteUserStory: (BuildContext context) {
|
|
||||||
context.go(homePage);
|
|
||||||
},
|
|
||||||
|
|
||||||
// (RECOMMENDED) Handle processing of the order details. This function
|
|
||||||
// should return true if the order was processed successfully, otherwise
|
|
||||||
// false.
|
|
||||||
//
|
|
||||||
// If this function is not provided, it is assumed that the order is
|
|
||||||
// always processed successfully.
|
|
||||||
//
|
|
||||||
// Example use cases that could be implemented here:
|
|
||||||
// - Sending and storing the order on a server,
|
|
||||||
// - Processing payment (if the user decides to pay upfront).
|
|
||||||
// - And many more...
|
|
||||||
onCompleteOrderDetails:
|
|
||||||
(BuildContext context, OrderResult orderDetails) async {
|
|
||||||
...
|
|
||||||
},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
For more information about the component specific items please take a look at their repositories:
|
|
||||||
|
|
||||||
- [flutter_product_page](https://github.com/Iconica-Development/flutter_product_page/)
|
|
||||||
- [flutter_shopping_cart](https://github.com/Iconica-Development/flutter_shopping_cart)
|
|
||||||
- [flutter_order_details](https://github.com/Iconica-Development/flutter_order_details)
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
For a detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping/tree/main/example).
|
TODO: Include short and useful examples for package users. Add longer examples
|
||||||
|
to `/example` folder.
|
||||||
|
|
||||||
Or, you could run the example yourself:
|
```dart
|
||||||
```
|
const like = 'sample';
|
||||||
git clone https://github.com/Iconica-Development/flutter_shopping.git
|
|
||||||
|
|
||||||
cd flutter_shopping
|
|
||||||
|
|
||||||
cd example
|
|
||||||
|
|
||||||
flutter run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Issues
|
## Additional information
|
||||||
|
|
||||||
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_shopping) 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).
|
TODO: Tell users more about the package: where to find more information, how to
|
||||||
|
contribute to the package, how to file issues, what response they can expect
|
||||||
## Want to contribute
|
from the package authors, and more.
|
||||||
|
|
||||||
If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_shopping/pulls).
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
This flutter_shopping for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
|
|
||||||
|
|
BIN
packages/flutter_shopping/assets/placeholder.jpg
Normal file
BIN
packages/flutter_shopping/assets/placeholder.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 MiB |
|
@ -9,7 +9,6 @@
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
migrate_working_dir/
|
migrate_working_dir/
|
||||||
.metadata
|
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
|
@ -20,7 +19,7 @@ migrate_working_dir/
|
||||||
# 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
|
||||||
# is commented out by default.
|
# is commented out by default.
|
||||||
.vscode/
|
#.vscode/
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
# Flutter/Dart/Pub related
|
||||||
**/doc/api/
|
**/doc/api/
|
||||||
|
@ -28,11 +27,9 @@ migrate_working_dir/
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
.flutter-plugins
|
.flutter-plugins
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
.packages
|
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
pubspec.lock
|
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
@ -44,6 +41,3 @@ app.*.map.json
|
||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
# env
|
|
||||||
*dotenv
|
|
28
packages/flutter_shopping/example/analysis_options.yaml
Normal file
28
packages/flutter_shopping/example/analysis_options.yaml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
BIN
packages/flutter_shopping/example/fonts/Avenir-Regular.ttf
Normal file
BIN
packages/flutter_shopping/example/fonts/Avenir-Regular.ttf
Normal file
Binary file not shown.
BIN
packages/flutter_shopping/example/fonts/Merriweather-Regular.ttf
Normal file
BIN
packages/flutter_shopping/example/fonts/Merriweather-Regular.ttf
Normal file
Binary file not shown.
30
packages/flutter_shopping/example/lib/main.dart
Normal file
30
packages/flutter_shopping/example/lib/main.dart
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import 'package:example/theme.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_shopping/flutter_shopping.dart';
|
||||||
|
|
||||||
|
void main(List<String> args) {
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: theme,
|
||||||
|
home: const Home(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Home extends StatelessWidget {
|
||||||
|
const Home({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const FlutterShoppingNavigatorUserstory(
|
||||||
|
options: FlutterShoppingOptions(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
147
packages/flutter_shopping/example/lib/theme.dart
Normal file
147
packages/flutter_shopping/example/lib/theme.dart
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const Color primaryColor = Color(0xFF71C6D1);
|
||||||
|
|
||||||
|
ThemeData theme = ThemeData(
|
||||||
|
actionIconTheme: ActionIconThemeData(
|
||||||
|
backButtonIconBuilder: (context) {
|
||||||
|
return const Icon(Icons.arrow_back_ios_new_rounded);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
scaffoldBackgroundColor: const Color(0xFFFAF9F6),
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
checkboxTheme: CheckboxThemeData(
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Color(0xFF8D8D8D),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
fillColor: WidgetStateProperty.resolveWith<Color>(
|
||||||
|
(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.selected)) {
|
||||||
|
return primaryColor;
|
||||||
|
}
|
||||||
|
return const Color(0xFFEEEEEE);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
switchTheme: SwitchThemeData(
|
||||||
|
trackColor:
|
||||||
|
WidgetStateProperty.resolveWith<Color>((Set<WidgetState> states) {
|
||||||
|
if (!states.contains(WidgetState.selected)) {
|
||||||
|
return const Color(0xFF8D8D8D);
|
||||||
|
}
|
||||||
|
return primaryColor;
|
||||||
|
}),
|
||||||
|
thumbColor: const WidgetStatePropertyAll(
|
||||||
|
Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
centerTitle: true,
|
||||||
|
iconTheme: IconThemeData(
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Color(0xFF212121),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 24,
|
||||||
|
color: Color(0xFF71C6D1),
|
||||||
|
fontFamily: "Merriweather",
|
||||||
|
),
|
||||||
|
actionsIconTheme: IconThemeData()),
|
||||||
|
fontFamily: "Merriweather",
|
||||||
|
useMaterial3: false,
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
headlineSmall: TextStyle(
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
headlineLarge: TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 24,
|
||||||
|
color: Color(0xFF71C6D1),
|
||||||
|
),
|
||||||
|
|
||||||
|
displayLarge: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 20,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
displayMedium: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 18,
|
||||||
|
color: Color(0xFF71C6D1),
|
||||||
|
),
|
||||||
|
displaySmall: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
|
||||||
|
// TITLE
|
||||||
|
titleSmall: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
titleMedium: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
titleLarge: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 20,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
|
||||||
|
// LABEL
|
||||||
|
labelSmall: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF8D8D8D),
|
||||||
|
),
|
||||||
|
|
||||||
|
// BODY
|
||||||
|
bodySmall: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
bodyMedium: TextStyle(
|
||||||
|
fontFamily: "Avenir",
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
radioTheme: RadioThemeData(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: 0,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
fillColor: WidgetStateProperty.resolveWith<Color>(
|
||||||
|
(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.selected)) {
|
||||||
|
return primaryColor;
|
||||||
|
}
|
||||||
|
return Colors.black;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
colorScheme: const ColorScheme.light(
|
||||||
|
primary: primaryColor,
|
||||||
|
),
|
||||||
|
);
|
34
packages/flutter_shopping/example/pubspec.yaml
Normal file
34
packages/flutter_shopping/example/pubspec.yaml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
name: example
|
||||||
|
description: "A new Flutter project."
|
||||||
|
|
||||||
|
publish_to: "none"
|
||||||
|
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.5.1
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_shopping:
|
||||||
|
path: ../
|
||||||
|
shopping_repository_interface:
|
||||||
|
path: ../../shopping_repository_interface
|
||||||
|
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
flutter_lints: ^4.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
fonts:
|
||||||
|
- family: Merriweather
|
||||||
|
fonts:
|
||||||
|
- asset: fonts/Merriweather-Regular.ttf
|
||||||
|
- family: Avenir
|
||||||
|
fonts:
|
||||||
|
- asset: fonts/Avenir-Regular.ttf
|
|
@ -1,9 +1,30 @@
|
||||||
/// Flutter Shopping
|
/// userstory
|
||||||
library flutter_shopping;
|
// ignore_for_file: directives_ordering
|
||||||
|
|
||||||
export "package:flutter_order_details/flutter_order_details.dart";
|
library;
|
||||||
export "package:flutter_product_page/flutter_product_page.dart";
|
|
||||||
export "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
|
||||||
|
|
||||||
export "src/configuration/shopping_configuration.dart";
|
export "package:shopping_repository_interface/shopping_repository_interface.dart";
|
||||||
export "src/flutter_shopping_navigator_userstory.dart";
|
export "src/flutter_shopping_navigator_userstory.dart";
|
||||||
|
|
||||||
|
/// screens
|
||||||
|
export "src/screens/shopping_screen.dart";
|
||||||
|
export "src/screens/shopping_cart_screen.dart";
|
||||||
|
export "src/screens/filter_screen.dart";
|
||||||
|
export "src/screens/personal_information_screen.dart";
|
||||||
|
export "src/screens/date_time_information_screen.dart";
|
||||||
|
export "src/screens/payment_options_screen.dart";
|
||||||
|
|
||||||
|
/// models
|
||||||
|
export "src/models/shopping_translations.dart";
|
||||||
|
export "src/models/flutter_shopping_options.dart";
|
||||||
|
|
||||||
|
/// widgets
|
||||||
|
export "src/widgets/primary_button.dart";
|
||||||
|
export "src/widgets/shop_selector.dart";
|
||||||
|
export "src/widgets/filter_item.dart";
|
||||||
|
export "src/widgets/shopping_cart_item.dart";
|
||||||
|
export "src/widgets/info_bottomsheet.dart";
|
||||||
|
export "src/widgets/product_item_list.dart";
|
||||||
|
export "src/widgets/product_item.dart";
|
||||||
|
export "src/widgets/shop_selector_item.dart";
|
||||||
|
export "src/widgets/weekly_offer.dart";
|
||||||
|
|
|
@ -1,240 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping/flutter_shopping.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// configuration for the shopping userstory
|
|
||||||
class ShoppingConfiguration {
|
|
||||||
/// constructor for the userstory configuration
|
|
||||||
const ShoppingConfiguration({
|
|
||||||
/// ProductPage configurations
|
|
||||||
required this.shoppingService,
|
|
||||||
this.onGetProducts,
|
|
||||||
this.onGetShops,
|
|
||||||
this.onAddToCart,
|
|
||||||
this.onNavigateToShoppingCart,
|
|
||||||
this.getProductsInShoppingCart,
|
|
||||||
this.shoppingCartButtonBuilder,
|
|
||||||
this.productBuilder,
|
|
||||||
this.onShopSelectionChange,
|
|
||||||
this.productPageTranslations,
|
|
||||||
this.shopSelectorStyle,
|
|
||||||
this.productPagePagePadding,
|
|
||||||
this.productPageAppBarBuilder,
|
|
||||||
this.bottomNavigationBarBuilder,
|
|
||||||
this.onProductDetail,
|
|
||||||
this.discountDescription,
|
|
||||||
this.noContentBuilder,
|
|
||||||
this.errorBuilder,
|
|
||||||
this.categoryListBuilder,
|
|
||||||
this.shopselectorBuilder,
|
|
||||||
this.discountBuilder,
|
|
||||||
this.selectedCategoryBuilder,
|
|
||||||
|
|
||||||
/// ShoppingCart configurations
|
|
||||||
this.onConfirmOrder,
|
|
||||||
this.productItemBuilder,
|
|
||||||
this.confirmOrderButtonBuilder,
|
|
||||||
this.confirmOrderButtonHeight,
|
|
||||||
this.sumBottomSheetBuilder,
|
|
||||||
this.sumBottomSheetHeight,
|
|
||||||
this.titleBuilder,
|
|
||||||
this.shoppingCartTranslations,
|
|
||||||
this.shoppingCartPagePadding,
|
|
||||||
this.shoppingCartBottomPadding,
|
|
||||||
this.shoppingCartAppBarBuilder,
|
|
||||||
|
|
||||||
/// OrderDetail configurations
|
|
||||||
this.onNextStep,
|
|
||||||
this.onStepsCompleted,
|
|
||||||
this.onCompleteOrderDetails,
|
|
||||||
this.pages,
|
|
||||||
this.orderDetailTranslations,
|
|
||||||
this.orderDetailAppBarBuilder,
|
|
||||||
this.orderDetailNextbuttonBuilder,
|
|
||||||
this.orderSuccessBuilder,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The service that will be used for the userstory
|
|
||||||
final ShoppingService shoppingService;
|
|
||||||
|
|
||||||
/// Builder for the list of selected categories
|
|
||||||
final Widget Function(ProductPageConfiguration configuration)?
|
|
||||||
selectedCategoryBuilder;
|
|
||||||
|
|
||||||
/// Function that will be called when the products are requested
|
|
||||||
final Future<List<Product>> Function(String shopId)? onGetProducts;
|
|
||||||
|
|
||||||
/// Function that will be called when the shops are requested
|
|
||||||
final Future<List<Shop>> Function()? onGetShops;
|
|
||||||
|
|
||||||
/// Function that will be called when an item is added to the shopping cart
|
|
||||||
final Function(Product)? onAddToCart;
|
|
||||||
|
|
||||||
/// Function that will be called when the user navigates to the shopping cart
|
|
||||||
final Function()? onNavigateToShoppingCart;
|
|
||||||
|
|
||||||
/// Function that will be called to get the amount of
|
|
||||||
/// products in the shopping cart
|
|
||||||
final int Function()? getProductsInShoppingCart;
|
|
||||||
|
|
||||||
/// Default shopping cart button builder
|
|
||||||
final Widget Function(BuildContext, ProductPageConfiguration)?
|
|
||||||
shoppingCartButtonBuilder;
|
|
||||||
|
|
||||||
/// ProductPage item builder
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext,
|
|
||||||
Product,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
)? productBuilder;
|
|
||||||
|
|
||||||
/// Function that will be called when the shop selection changes
|
|
||||||
final Function(Shop)? onShopSelectionChange;
|
|
||||||
|
|
||||||
/// Translations for the product page
|
|
||||||
final ProductPageTranslations? productPageTranslations;
|
|
||||||
|
|
||||||
/// Shop selector style
|
|
||||||
final ShopSelectorStyle? shopSelectorStyle;
|
|
||||||
|
|
||||||
/// ProductPage padding
|
|
||||||
final EdgeInsets? productPagePagePadding;
|
|
||||||
|
|
||||||
/// AppBar builder
|
|
||||||
final AppBar Function(BuildContext)? productPageAppBarBuilder;
|
|
||||||
|
|
||||||
/// BottomNavigationBarBuilder
|
|
||||||
final Widget? bottomNavigationBarBuilder;
|
|
||||||
|
|
||||||
/// Function that will be called when the product detail is requested
|
|
||||||
final Function(BuildContext, Product, String)? onProductDetail;
|
|
||||||
|
|
||||||
/// Function that will be called when the discount description is requested
|
|
||||||
final String Function(Product)? discountDescription;
|
|
||||||
|
|
||||||
/// Function that will be called when there are no products
|
|
||||||
final Widget Function(BuildContext)? noContentBuilder;
|
|
||||||
|
|
||||||
/// Function that will be called when there is an error
|
|
||||||
final Widget Function(BuildContext, Object?)? errorBuilder;
|
|
||||||
|
|
||||||
/// Builder for the shop selector. This builder is used to build the shop
|
|
||||||
/// selector that will be displayed in the product page.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
List<Shop> shops,
|
|
||||||
Function(Shop shop) onShopSelectionChange,
|
|
||||||
)? shopselectorBuilder;
|
|
||||||
|
|
||||||
/// Builder for the discount widget. This builder is used to build the
|
|
||||||
/// discount widget that will be displayed in the product page.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
List<Product> discountedProducts,
|
|
||||||
)? discountBuilder;
|
|
||||||
|
|
||||||
/// Builder for the list of items that are displayed in the product page.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
List<Product> products,
|
|
||||||
)? categoryListBuilder;
|
|
||||||
|
|
||||||
/// Function that will be called when the order button on
|
|
||||||
/// the shopping cart page is pressed
|
|
||||||
final Function(List<Product>)? onConfirmOrder;
|
|
||||||
|
|
||||||
/// Shopping cart item builder
|
|
||||||
final Widget Function(BuildContext, Product, ShoppingCartConfig)?
|
|
||||||
productItemBuilder;
|
|
||||||
|
|
||||||
/// Shopping cart confirm order button builder
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext,
|
|
||||||
ShoppingCartConfig,
|
|
||||||
dynamic Function(List<Product>),
|
|
||||||
)? confirmOrderButtonBuilder;
|
|
||||||
|
|
||||||
/// The height of the confirm order button
|
|
||||||
/// This will not set the height of the button itself
|
|
||||||
/// this is only used to create some extra space on the bottom
|
|
||||||
/// of the product list so the button doesn't overlap with the
|
|
||||||
/// last product
|
|
||||||
final double? confirmOrderButtonHeight;
|
|
||||||
|
|
||||||
/// Shopping cart sum bottom sheet builder
|
|
||||||
final Widget Function(BuildContext, ShoppingCartConfig)?
|
|
||||||
sumBottomSheetBuilder;
|
|
||||||
|
|
||||||
/// The height of the sum bottom sheet
|
|
||||||
/// This will not set the height of the sheet itself
|
|
||||||
/// this is only used to create some extra space on the bottom
|
|
||||||
/// of the product list so the sheet doesn't overlap with the
|
|
||||||
/// last product
|
|
||||||
final double? sumBottomSheetHeight;
|
|
||||||
|
|
||||||
/// Function to override the title on the shopping cart screen
|
|
||||||
final Widget Function(BuildContext, String)? titleBuilder;
|
|
||||||
|
|
||||||
/// Shopping cart translations
|
|
||||||
final ShoppingCartTranslations? shoppingCartTranslations;
|
|
||||||
|
|
||||||
/// Shopping cart page padding
|
|
||||||
final EdgeInsets? shoppingCartPagePadding;
|
|
||||||
|
|
||||||
/// Shopping cart bottom padding
|
|
||||||
final EdgeInsets? shoppingCartBottomPadding;
|
|
||||||
|
|
||||||
/// Shopping cart app bar builder
|
|
||||||
final AppBar Function(BuildContext)? shoppingCartAppBarBuilder;
|
|
||||||
|
|
||||||
/// Function that gets called when the user navigates to the next
|
|
||||||
/// step of the order details
|
|
||||||
final dynamic Function(
|
|
||||||
int,
|
|
||||||
Map<String, dynamic>,
|
|
||||||
FlutterFormController controller,
|
|
||||||
)? onNextStep;
|
|
||||||
|
|
||||||
/// Function that gets called when the Navigates
|
|
||||||
/// to the order confirmationp page
|
|
||||||
final dynamic Function(
|
|
||||||
String,
|
|
||||||
List<Product>,
|
|
||||||
Map<int, Map<String, dynamic>>,
|
|
||||||
OrderDetailConfiguration,
|
|
||||||
)? onStepsCompleted;
|
|
||||||
|
|
||||||
/// Function that gets called when pressing the complete order
|
|
||||||
/// button on the confirmation page
|
|
||||||
final Function(BuildContext, OrderDetailConfiguration)?
|
|
||||||
onCompleteOrderDetails;
|
|
||||||
|
|
||||||
/// The order detail pages that are used in the order detail screen
|
|
||||||
final List<FlutterFormPage> Function(BuildContext)? pages;
|
|
||||||
|
|
||||||
/// The translations for the order detail screen
|
|
||||||
final OrderDetailTranslations? orderDetailTranslations;
|
|
||||||
|
|
||||||
/// The app bar for the order detail screen
|
|
||||||
final AppBar Function(BuildContext, String)? orderDetailAppBarBuilder;
|
|
||||||
|
|
||||||
/// The builder for the next button on the order detail screen
|
|
||||||
final Widget Function(
|
|
||||||
int,
|
|
||||||
// ignore: avoid_positional_boolean_parameters
|
|
||||||
bool,
|
|
||||||
BuildContext,
|
|
||||||
OrderDetailConfiguration,
|
|
||||||
FlutterFormController,
|
|
||||||
)? orderDetailNextbuttonBuilder;
|
|
||||||
|
|
||||||
/// The builder for the order success screen
|
|
||||||
final Widget? Function(
|
|
||||||
BuildContext,
|
|
||||||
OrderDetailConfiguration,
|
|
||||||
Map<int, Map<String, dynamic>>,
|
|
||||||
)? orderSuccessBuilder;
|
|
||||||
}
|
|
|
@ -1,235 +1,109 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_shopping/flutter_shopping.dart";
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
import "package:flutter_shopping_local/flutter_shopping_local.dart";
|
|
||||||
|
|
||||||
/// User story for the shopping navigator.
|
/// A userstory for the FlutterShoppingNavigator.
|
||||||
class ShoppingNavigatorUserStory extends StatelessWidget {
|
class FlutterShoppingNavigatorUserstory extends StatefulWidget {
|
||||||
/// Constructor for the shopping navigator user story.
|
/// Constructor for the FlutterShoppingNavigatorUserstory.
|
||||||
const ShoppingNavigatorUserStory({
|
const FlutterShoppingNavigatorUserstory({
|
||||||
this.shoppingConfiguration,
|
this.options = const FlutterShoppingOptions(),
|
||||||
|
this.translations = const ShoppingTranslations(),
|
||||||
|
this.shoppingService,
|
||||||
|
this.initialShopId,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Shopping configuration.
|
/// The options for the shopping navigator.
|
||||||
final ShoppingConfiguration? shoppingConfiguration;
|
final FlutterShoppingOptions options;
|
||||||
|
|
||||||
|
/// The shopping service.
|
||||||
|
final ShoppingService? shoppingService;
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
/// The initial shop id.
|
||||||
|
final String? initialShopId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ShoppingProductPage(
|
State<FlutterShoppingNavigatorUserstory> createState() =>
|
||||||
shoppingConfiguration: shoppingConfiguration ??
|
_FlutterShoppingNavigatorUserstoryState();
|
||||||
ShoppingConfiguration(
|
|
||||||
shoppingService: LocalShoppingService(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shopping product page.
|
class _FlutterShoppingNavigatorUserstoryState
|
||||||
class ShoppingProductPage extends StatelessWidget {
|
extends State<FlutterShoppingNavigatorUserstory> {
|
||||||
/// Constructor for the shopping product page.
|
late ShoppingService shoppingService;
|
||||||
const ShoppingProductPage({
|
|
||||||
required this.shoppingConfiguration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Shopping configuration.
|
|
||||||
final ShoppingConfiguration shoppingConfiguration;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
void initState() {
|
||||||
var service = shoppingConfiguration.shoppingService;
|
shoppingService = widget.shoppingService ?? ShoppingService();
|
||||||
return ProductPageScreen(
|
super.initState();
|
||||||
configuration: ProductPageConfiguration(
|
|
||||||
shoppingService: service,
|
|
||||||
shoppingCartButtonBuilder:
|
|
||||||
shoppingConfiguration.shoppingCartButtonBuilder,
|
|
||||||
productBuilder: shoppingConfiguration.productBuilder,
|
|
||||||
onShopSelectionChange: shoppingConfiguration.onShopSelectionChange,
|
|
||||||
translations: shoppingConfiguration.productPageTranslations ??
|
|
||||||
const ProductPageTranslations(),
|
|
||||||
shopSelectorStyle:
|
|
||||||
shoppingConfiguration.shopSelectorStyle ?? ShopSelectorStyle.row,
|
|
||||||
pagePadding: shoppingConfiguration.productPagePagePadding ??
|
|
||||||
const EdgeInsets.all(4),
|
|
||||||
appBarBuilder: shoppingConfiguration.productPageAppBarBuilder,
|
|
||||||
bottomNavigationBar: shoppingConfiguration.bottomNavigationBarBuilder,
|
|
||||||
onProductDetail: shoppingConfiguration.onProductDetail,
|
|
||||||
discountDescription: shoppingConfiguration.discountDescription,
|
|
||||||
noContentBuilder: shoppingConfiguration.noContentBuilder,
|
|
||||||
errorBuilder: shoppingConfiguration.errorBuilder,
|
|
||||||
shopselectorBuilder: shoppingConfiguration.shopselectorBuilder,
|
|
||||||
discountBuilder: shoppingConfiguration.discountBuilder,
|
|
||||||
categoryListBuilder: shoppingConfiguration.categoryListBuilder,
|
|
||||||
selectedCategoryBuilder: shoppingConfiguration.selectedCategoryBuilder,
|
|
||||||
shops: () async {
|
|
||||||
if (shoppingConfiguration.onGetShops != null) {
|
|
||||||
return shoppingConfiguration.onGetShops!();
|
|
||||||
} else {
|
|
||||||
return service.shopService.getShops();
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
getProducts: (shop) async {
|
|
||||||
if (shoppingConfiguration.onGetProducts != null) {
|
|
||||||
return shoppingConfiguration.onGetProducts!(shop.id);
|
|
||||||
} else {
|
|
||||||
return service.productService.getProducts(shop.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onAddToCart: (product) {
|
|
||||||
if (shoppingConfiguration.onAddToCart != null) {
|
|
||||||
shoppingConfiguration.onAddToCart!(product);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
return service.shoppingCartService.addProduct(product);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onNavigateToShoppingCart: () async {
|
|
||||||
if (shoppingConfiguration.onNavigateToShoppingCart != null) {
|
|
||||||
return shoppingConfiguration.onNavigateToShoppingCart!();
|
|
||||||
} else {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ShoppingCart(
|
|
||||||
shoppingConfiguration: shoppingConfiguration,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getProductsInShoppingCart: () {
|
|
||||||
if (shoppingConfiguration.getProductsInShoppingCart != null) {
|
|
||||||
return shoppingConfiguration.getProductsInShoppingCart!();
|
|
||||||
} else {
|
|
||||||
return service.shoppingCartService.countProducts();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shopping cart.
|
|
||||||
class ShoppingCart extends StatelessWidget {
|
|
||||||
/// Constructor for the shopping cart.
|
|
||||||
const ShoppingCart({
|
|
||||||
required this.shoppingConfiguration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Shopping configuration.
|
|
||||||
final ShoppingConfiguration shoppingConfiguration;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => shoppingScreen();
|
||||||
var service = shoppingConfiguration.shoppingService.shoppingCartService;
|
|
||||||
return ShoppingCartScreen(
|
Widget shoppingScreen() => ShoppingScreen(
|
||||||
configuration: ShoppingCartConfig(
|
initialShopId: widget.initialShopId,
|
||||||
service: service,
|
translations: widget.translations,
|
||||||
productItemBuilder: shoppingConfiguration.productItemBuilder,
|
shoppingService: shoppingService,
|
||||||
confirmOrderButtonBuilder:
|
options: widget.options,
|
||||||
shoppingConfiguration.confirmOrderButtonBuilder,
|
onFilterPressed: () async {
|
||||||
confirmOrderButtonHeight:
|
widget.options.onFilterPressed?.call() ?? await push(filterScreen());
|
||||||
shoppingConfiguration.confirmOrderButtonHeight ?? 100,
|
},
|
||||||
sumBottomSheetBuilder: shoppingConfiguration.sumBottomSheetBuilder,
|
onShoppingCartPressed: () async {
|
||||||
sumBottomSheetHeight: shoppingConfiguration.sumBottomSheetHeight ?? 100,
|
widget.options.onCartPressed?.call() ??
|
||||||
titleBuilder: shoppingConfiguration.titleBuilder,
|
await push(shoppingCartScreen());
|
||||||
translations: shoppingConfiguration.shoppingCartTranslations ??
|
|
||||||
const ShoppingCartTranslations(),
|
|
||||||
pagePadding: shoppingConfiguration.shoppingCartPagePadding ??
|
|
||||||
const EdgeInsets.symmetric(horizontal: 32),
|
|
||||||
bottomPadding: shoppingConfiguration.shoppingCartBottomPadding ??
|
|
||||||
const EdgeInsets.fromLTRB(44, 0, 44, 32),
|
|
||||||
appBarBuilder: shoppingConfiguration.shoppingCartAppBarBuilder,
|
|
||||||
onConfirmOrder: (products) async {
|
|
||||||
if (shoppingConfiguration.onConfirmOrder != null) {
|
|
||||||
return shoppingConfiguration.onConfirmOrder!(products);
|
|
||||||
} else {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ShoppingOrderDetails(
|
|
||||||
shoppingConfiguration: shoppingConfiguration,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget filterScreen() => FilterScreen(
|
||||||
|
translations: widget.translations,
|
||||||
|
shoppingService: shoppingService,
|
||||||
|
options: widget.options,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget shoppingCartScreen() => ShoppingCartScreen(
|
||||||
|
translations: widget.translations,
|
||||||
|
shoppingService: shoppingService,
|
||||||
|
options: widget.options,
|
||||||
|
onOrder: () async {
|
||||||
|
widget.options.onOrderPressed?.call() ??
|
||||||
|
await push(personalInformationScreen());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Widget personalInformationScreen() => PersonalInformationScreen(
|
||||||
|
translations: widget.translations,
|
||||||
|
options: widget.options,
|
||||||
|
onFinished: (value) async {
|
||||||
|
widget.options.getPersonalInformation?.call(value);
|
||||||
|
widget.options.onPersonalInformationPressed?.call() ??
|
||||||
|
await push(dateTimeInformationScreen());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget dateTimeInformationScreen() => DateTimeInformationScreen(
|
||||||
|
translations: widget.translations,
|
||||||
|
onFinished: (value) async {
|
||||||
|
widget.options.getDateTimeInformation?.call(value);
|
||||||
|
widget.options.onDateTimePressed?.call() ??
|
||||||
|
await push(paymentOptionsScreen());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget paymentOptionsScreen() => PaymentOptionsScreen(
|
||||||
|
translations: widget.translations,
|
||||||
|
onFinished: (value) async {
|
||||||
|
widget.options.getPaymentInformation?.call(value);
|
||||||
|
widget.options.onPaymentPressed?.call() ?? await popUntil();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> push(Widget screen) async {
|
||||||
|
await Navigator.of(context)
|
||||||
|
.push(MaterialPageRoute(builder: (context) => screen));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> popUntil() async {
|
||||||
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shopping order details.
|
|
||||||
class ShoppingOrderDetails extends StatelessWidget {
|
|
||||||
/// Constructor for the shopping order details.
|
|
||||||
const ShoppingOrderDetails({
|
|
||||||
required this.shoppingConfiguration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Shopping configuration.
|
|
||||||
final ShoppingConfiguration shoppingConfiguration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => OrderDetailScreen(
|
|
||||||
configuration: OrderDetailConfiguration(
|
|
||||||
shoppingService: shoppingConfiguration.shoppingService,
|
|
||||||
pages: shoppingConfiguration.pages,
|
|
||||||
translations: shoppingConfiguration.orderDetailTranslations ??
|
|
||||||
const OrderDetailTranslations(),
|
|
||||||
appBarBuilder: shoppingConfiguration.orderDetailAppBarBuilder,
|
|
||||||
nextbuttonBuilder: shoppingConfiguration.orderDetailNextbuttonBuilder,
|
|
||||||
orderSuccessBuilder: (context, configuration, data) =>
|
|
||||||
shoppingConfiguration.orderSuccessBuilder
|
|
||||||
?.call(context, configuration, data) ??
|
|
||||||
DefaultOrderSucces(
|
|
||||||
configuration: configuration,
|
|
||||||
orderDetails: data,
|
|
||||||
),
|
|
||||||
onNextStep: (currentStep, data, controller) async {
|
|
||||||
if (shoppingConfiguration.onNextStep != null) {
|
|
||||||
return shoppingConfiguration.onNextStep!(
|
|
||||||
currentStep,
|
|
||||||
data,
|
|
||||||
controller,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await controller.autoNextStep();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onStepsCompleted: (shopId, products, data, configuration) async {
|
|
||||||
if (shoppingConfiguration.onStepsCompleted != null) {
|
|
||||||
return shoppingConfiguration.onStepsCompleted!(
|
|
||||||
shopId,
|
|
||||||
products,
|
|
||||||
data,
|
|
||||||
configuration,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Navigator.of(context).pushReplacement(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => DefaultOrderSucces(
|
|
||||||
configuration: configuration,
|
|
||||||
orderDetails: data,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCompleteOrderDetails: (context, configuration) async {
|
|
||||||
if (shoppingConfiguration.onCompleteOrderDetails != null) {
|
|
||||||
return shoppingConfiguration.onCompleteOrderDetails!(
|
|
||||||
context,
|
|
||||||
configuration,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
shoppingConfiguration.shoppingService.shoppingCartService.clear();
|
|
||||||
return Navigator.of(context).pushReplacement(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ShoppingProductPage(
|
|
||||||
shoppingConfiguration: shoppingConfiguration,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:shopping_repository_interface/shopping_repository_interface.dart";
|
||||||
|
|
||||||
|
/// The options for the flutter shopping package.
|
||||||
|
class FlutterShoppingOptions {
|
||||||
|
/// The constructor for the FlutterShoppingOptions.
|
||||||
|
const FlutterShoppingOptions({
|
||||||
|
this.shoppingScreenAppbarBuilder,
|
||||||
|
this.filterChipbuilder,
|
||||||
|
this.weeklyOfferBuilder,
|
||||||
|
this.productItemBuilder,
|
||||||
|
this.primaryButtonBuilder,
|
||||||
|
this.shopSelectorItemBuilder,
|
||||||
|
this.filterScreenAppbarBuilder,
|
||||||
|
this.filterItemBuilder,
|
||||||
|
this.shoppingCartItemBuilder,
|
||||||
|
this.onInfoPressed,
|
||||||
|
this.informationInputDecoration,
|
||||||
|
this.onFilterPressed,
|
||||||
|
this.onCartPressed,
|
||||||
|
this.onOrderPressed,
|
||||||
|
this.onPersonalInformationPressed,
|
||||||
|
this.onDateTimePressed,
|
||||||
|
this.onPaymentPressed,
|
||||||
|
this.shoppingScreenDrawer,
|
||||||
|
this.getPersonalInformation,
|
||||||
|
this.getDateTimeInformation,
|
||||||
|
this.getPaymentInformation,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The appbar builder for the shopping screen.
|
||||||
|
final AppBarBuilder? shoppingScreenAppbarBuilder;
|
||||||
|
|
||||||
|
/// The weekly offer builder for the shopping screen.
|
||||||
|
final ItemBuilder? weeklyOfferBuilder;
|
||||||
|
|
||||||
|
/// The product item builder for the shopping screen.
|
||||||
|
final ItemBuilder? productItemBuilder;
|
||||||
|
|
||||||
|
/// The filter chip builder for the shopping screen.
|
||||||
|
final Widget? Function(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
Function() onDelete,
|
||||||
|
)? filterChipbuilder;
|
||||||
|
|
||||||
|
/// The primary button builder.
|
||||||
|
final Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
// ignore: avoid_positional_boolean_parameters
|
||||||
|
bool enabled,
|
||||||
|
String title,
|
||||||
|
Function() onPressed,
|
||||||
|
)? primaryButtonBuilder;
|
||||||
|
|
||||||
|
/// shop selector item builder.
|
||||||
|
final Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
Shop shop,
|
||||||
|
Function(Shop) onSelected,
|
||||||
|
)? shopSelectorItemBuilder;
|
||||||
|
|
||||||
|
/// The appbar builder for the filter screen.
|
||||||
|
final AppBarBuilder? filterScreenAppbarBuilder;
|
||||||
|
|
||||||
|
/// The filter item builder.
|
||||||
|
final Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
Function() onDeselect,
|
||||||
|
Function() onSelect,
|
||||||
|
// ignore: avoid_positional_boolean_parameters
|
||||||
|
bool currentValue,
|
||||||
|
)? filterItemBuilder;
|
||||||
|
|
||||||
|
/// The shopping cart item builder.
|
||||||
|
final Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
Product product,
|
||||||
|
Function() onAddToCart,
|
||||||
|
Function() onRemoveFromCart,
|
||||||
|
)? shoppingCartItemBuilder;
|
||||||
|
|
||||||
|
/// The on info pressed function.
|
||||||
|
final Function(Product product)? onInfoPressed;
|
||||||
|
|
||||||
|
/// The input decoration for the information.
|
||||||
|
final InputDecoration? Function(BuildContext context, String hintText)?
|
||||||
|
informationInputDecoration;
|
||||||
|
|
||||||
|
/// on filter pressed.
|
||||||
|
final Function()? onFilterPressed;
|
||||||
|
|
||||||
|
/// on cart pressed.
|
||||||
|
final Function()? onCartPressed;
|
||||||
|
|
||||||
|
/// on order pressed.
|
||||||
|
final Function()? onOrderPressed;
|
||||||
|
|
||||||
|
/// on personal information pressed.
|
||||||
|
final Function()? onPersonalInformationPressed;
|
||||||
|
|
||||||
|
/// on address pressed.
|
||||||
|
final Function()? onDateTimePressed;
|
||||||
|
|
||||||
|
/// on payment pressed.
|
||||||
|
final Function()? onPaymentPressed;
|
||||||
|
|
||||||
|
/// The shopping screen drawer.
|
||||||
|
final Widget? shoppingScreenDrawer;
|
||||||
|
|
||||||
|
/// The function to get the personal information.
|
||||||
|
final Function(Map<String, dynamic> value)? getPersonalInformation;
|
||||||
|
|
||||||
|
/// The function to get the address information.
|
||||||
|
final Function(Map<String, dynamic> value)? getDateTimeInformation;
|
||||||
|
|
||||||
|
/// The function to get the payment information.
|
||||||
|
final Function(Map<String, dynamic> value)? getPaymentInformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appbar builder.
|
||||||
|
typedef AppBarBuilder = AppBar Function(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
Function()? onFilterPressed,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The item builder.
|
||||||
|
typedef ItemBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
Product product,
|
||||||
|
);
|
|
@ -0,0 +1,225 @@
|
||||||
|
/// Translations for the Shopping package.
|
||||||
|
class ShoppingTranslations {
|
||||||
|
/// Constructor for the Shopping translations.
|
||||||
|
const ShoppingTranslations({
|
||||||
|
/// screen titles
|
||||||
|
this.filterTitle = "filter",
|
||||||
|
this.shoppingCartTitle = "Shopping cart",
|
||||||
|
this.personalInformationTitle = "information",
|
||||||
|
this.dateTimeInformationTitle = "information",
|
||||||
|
this.paymentOptionsTitle = "information",
|
||||||
|
|
||||||
|
/// button text
|
||||||
|
this.orderButton = "Order",
|
||||||
|
this.closeInfo = "Close",
|
||||||
|
this.viewShoppingCart = "View shopping cart",
|
||||||
|
this.personalInformationButton = "Choose date and time",
|
||||||
|
this.paymentButton = "Next",
|
||||||
|
this.datetimeInformationButton = "Next",
|
||||||
|
|
||||||
|
/// body text
|
||||||
|
this.weeklyOffer = "Weekly offer",
|
||||||
|
this.whatWouldyouLikeToOrder = "What would you like to order?",
|
||||||
|
this.shoppingCartProducts = "Products",
|
||||||
|
this.shoppingCartTotal = "Subtotal",
|
||||||
|
this.shoppingCartCurrency = "€",
|
||||||
|
this.personalInformationName = "What’s your name?",
|
||||||
|
this.personalInformationAddress = "What’s your address?",
|
||||||
|
this.personalInformationPhone = "What’s your phone number?",
|
||||||
|
this.personalInformationEmail = "What’s your email address?",
|
||||||
|
this.personalInformationComment = "Do you have any comments?",
|
||||||
|
this.paymenttitle = "Payment method",
|
||||||
|
this.paymentExplainer =
|
||||||
|
"Choose when you would like to to pay for the order.",
|
||||||
|
this.payNow = "Pay now",
|
||||||
|
this.payLater = "Pay later",
|
||||||
|
this.chooseDateAndTime =
|
||||||
|
"When and at what time would you like to pick up your order?",
|
||||||
|
this.selectDay = "Select a day",
|
||||||
|
this.dayToday = "Today",
|
||||||
|
this.dayTomorrow = "Tomorrow",
|
||||||
|
this.morning = "Morning",
|
||||||
|
this.afternoon = "Afternoon",
|
||||||
|
this.selectTime = "Select a time",
|
||||||
|
|
||||||
|
/// error messages
|
||||||
|
this.nameRequired = "Name is required",
|
||||||
|
this.addressRequired = "Please enter a street and house number",
|
||||||
|
this.postalCodeRequired = "Please enter your postal code",
|
||||||
|
this.cityRequired = "Please enter your city",
|
||||||
|
this.phoneRequired = "Please enter your phone number",
|
||||||
|
this.emailRequired = "Email is required",
|
||||||
|
this.invalidEmail = "Please fill in a valid email address",
|
||||||
|
this.paymentError = "Please select a payment method",
|
||||||
|
this.invalidAdress = "Invalid street and house number",
|
||||||
|
this.invalidPostalCode = "Invalid postal code",
|
||||||
|
this.invalidPhoneLength = "Invalid phone number length",
|
||||||
|
this.phoneContainsLettersError = "Phone number can only contain digits",
|
||||||
|
this.selectDayError = "Please select a day",
|
||||||
|
|
||||||
|
/// hint text
|
||||||
|
this.nameHint = "full name",
|
||||||
|
this.addressHint = "street name and number",
|
||||||
|
this.postalCodeHint = "postal code",
|
||||||
|
this.cityHint = "city",
|
||||||
|
this.phoneHint = "phone number",
|
||||||
|
this.emailHint = "email address",
|
||||||
|
this.commentHint = "optional",
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Appbar title for filter screen.
|
||||||
|
final String filterTitle;
|
||||||
|
|
||||||
|
/// Appbar title for shopping cart screen.
|
||||||
|
final String shoppingCartTitle;
|
||||||
|
|
||||||
|
/// Appbar title for personal information screen.
|
||||||
|
final String personalInformationTitle;
|
||||||
|
|
||||||
|
/// Text for the order button.
|
||||||
|
final String orderButton;
|
||||||
|
|
||||||
|
/// Text for the close info button.
|
||||||
|
final String closeInfo;
|
||||||
|
|
||||||
|
/// Text for the view shopping cart button.
|
||||||
|
final String viewShoppingCart;
|
||||||
|
|
||||||
|
/// Text for the personal information button.
|
||||||
|
final String personalInformationButton;
|
||||||
|
|
||||||
|
/// Text for the weekly offer.
|
||||||
|
final String weeklyOffer;
|
||||||
|
|
||||||
|
/// Text for the question what would you like to order.
|
||||||
|
final String whatWouldyouLikeToOrder;
|
||||||
|
|
||||||
|
/// Text for the products.
|
||||||
|
final String shoppingCartProducts;
|
||||||
|
|
||||||
|
/// Text for the total price in the shopping cart.
|
||||||
|
final String shoppingCartTotal;
|
||||||
|
|
||||||
|
/// The currency that is being used
|
||||||
|
final String shoppingCartCurrency;
|
||||||
|
|
||||||
|
/// Text for the personal information fields.
|
||||||
|
final String personalInformationName;
|
||||||
|
|
||||||
|
/// Text for the personal information fields.
|
||||||
|
final String personalInformationAddress;
|
||||||
|
|
||||||
|
/// Text for the personal information fields.
|
||||||
|
final String personalInformationPhone;
|
||||||
|
|
||||||
|
/// Text for the personal information fields.
|
||||||
|
final String personalInformationEmail;
|
||||||
|
|
||||||
|
/// Text for the personal information fields.
|
||||||
|
final String personalInformationComment;
|
||||||
|
|
||||||
|
/// Text for the name required field.
|
||||||
|
final String nameRequired;
|
||||||
|
|
||||||
|
/// Text for the address field.
|
||||||
|
final String addressRequired;
|
||||||
|
|
||||||
|
/// Text for the postal code field.
|
||||||
|
final String postalCodeRequired;
|
||||||
|
|
||||||
|
/// Text for the city field.
|
||||||
|
final String cityRequired;
|
||||||
|
|
||||||
|
/// Text for the phone field.
|
||||||
|
final String phoneRequired;
|
||||||
|
|
||||||
|
/// Text for the email field.
|
||||||
|
final String emailRequired;
|
||||||
|
|
||||||
|
/// Text for the invalid email field.
|
||||||
|
final String invalidEmail;
|
||||||
|
|
||||||
|
/// Text for the name hint.
|
||||||
|
final String nameHint;
|
||||||
|
|
||||||
|
/// Text for the address hint.
|
||||||
|
final String addressHint;
|
||||||
|
|
||||||
|
/// Text for the postal code hint.
|
||||||
|
final String postalCodeHint;
|
||||||
|
|
||||||
|
/// Text for the city hint.
|
||||||
|
final String cityHint;
|
||||||
|
|
||||||
|
/// Text for the phone hint.
|
||||||
|
final String phoneHint;
|
||||||
|
|
||||||
|
/// Text for the email hint.
|
||||||
|
final String emailHint;
|
||||||
|
|
||||||
|
/// Text for the comment hint.
|
||||||
|
final String commentHint;
|
||||||
|
|
||||||
|
/// Text for the date and time information title.
|
||||||
|
final String dateTimeInformationTitle;
|
||||||
|
|
||||||
|
/// Text for the payment options title.
|
||||||
|
final String paymentOptionsTitle;
|
||||||
|
|
||||||
|
/// Text for the payment error.
|
||||||
|
final String paymentError;
|
||||||
|
|
||||||
|
/// Text for the payment title.
|
||||||
|
final String paymenttitle;
|
||||||
|
|
||||||
|
/// Text for the payment explainer.
|
||||||
|
final String paymentExplainer;
|
||||||
|
|
||||||
|
/// Text for the pay now button.
|
||||||
|
final String payNow;
|
||||||
|
|
||||||
|
/// Text for the pay later button.
|
||||||
|
final String payLater;
|
||||||
|
|
||||||
|
/// Text for the payment button.
|
||||||
|
final String paymentButton;
|
||||||
|
|
||||||
|
/// Text for the invalid address.
|
||||||
|
final String invalidAdress;
|
||||||
|
|
||||||
|
/// Text for the invalid postal code.
|
||||||
|
final String invalidPostalCode;
|
||||||
|
|
||||||
|
/// Text for the invalid phone length.
|
||||||
|
final String invalidPhoneLength;
|
||||||
|
|
||||||
|
/// Text for the phone contains letters error.
|
||||||
|
final String phoneContainsLettersError;
|
||||||
|
|
||||||
|
/// Text for the select day error.
|
||||||
|
final String datetimeInformationButton;
|
||||||
|
|
||||||
|
/// Text for the choose date and time.
|
||||||
|
final String chooseDateAndTime;
|
||||||
|
|
||||||
|
/// Text for the select day.
|
||||||
|
final String selectDay;
|
||||||
|
|
||||||
|
/// Text for the select day error.
|
||||||
|
final String selectDayError;
|
||||||
|
|
||||||
|
/// Text for the day today.
|
||||||
|
final String dayToday;
|
||||||
|
|
||||||
|
/// Text for the day tomorrow.
|
||||||
|
final String dayTomorrow;
|
||||||
|
|
||||||
|
/// Text for the morning.
|
||||||
|
final String morning;
|
||||||
|
|
||||||
|
/// Text for the afternoon.
|
||||||
|
final String afternoon;
|
||||||
|
|
||||||
|
/// Text for the select time.
|
||||||
|
final String selectTime;
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
import "package:animated_toggle/animated_toggle.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_form_wizard/flutter_form.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// The morning times.
|
||||||
|
List<String> morningTimes = <String>[
|
||||||
|
"09:00",
|
||||||
|
"09:15",
|
||||||
|
"09:30",
|
||||||
|
"09:45",
|
||||||
|
"10:00",
|
||||||
|
"10:15",
|
||||||
|
"10:30",
|
||||||
|
"10:45",
|
||||||
|
"11:00",
|
||||||
|
"11:15",
|
||||||
|
"11:30",
|
||||||
|
"11:45",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The afternoon times.
|
||||||
|
List<String> afternoonTimes = <String>[
|
||||||
|
"12:00",
|
||||||
|
"12:15",
|
||||||
|
"12:30",
|
||||||
|
"12:45",
|
||||||
|
"13:00",
|
||||||
|
"13:15",
|
||||||
|
"13:30",
|
||||||
|
"13:45",
|
||||||
|
"14:00",
|
||||||
|
"14:15",
|
||||||
|
"14:30",
|
||||||
|
"14:45",
|
||||||
|
"15:00",
|
||||||
|
"15:15",
|
||||||
|
"15:30",
|
||||||
|
"15:45",
|
||||||
|
"16:00",
|
||||||
|
"16:15",
|
||||||
|
"16:30",
|
||||||
|
"16:45",
|
||||||
|
"17:00",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The personal information page.
|
||||||
|
class DateTimeInformationScreen extends StatefulWidget {
|
||||||
|
/// Constructor for the PersonalInformationScreen.
|
||||||
|
const DateTimeInformationScreen({
|
||||||
|
required this.translations,
|
||||||
|
required this.onFinished,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
/// The on finished function.
|
||||||
|
final Function(Map<String, dynamic>) onFinished;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DateTimeInformationScreen> createState() =>
|
||||||
|
_DateTimeInformationScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateTimeInformationScreenState extends State<DateTimeInformationScreen> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
var dateTimeInformationController = FlutterFormController();
|
||||||
|
var multipleChoiceController = FlutterFormInputMultipleChoiceController(
|
||||||
|
id: "multipleChoice",
|
||||||
|
mandatory: true,
|
||||||
|
);
|
||||||
|
InputDecoration dropdownInputDecoration(String hint) => InputDecoration(
|
||||||
|
hintStyle: theme.textTheme.bodySmall,
|
||||||
|
hintText: hint,
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var switchStatus = ValueNotifier(false);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(widget.translations.dateTimeInformationTitle),
|
||||||
|
),
|
||||||
|
body: FlutterForm(
|
||||||
|
formController: dateTimeInformationController,
|
||||||
|
options: FlutterFormOptions(
|
||||||
|
pages: [
|
||||||
|
FlutterFormPage(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Text(
|
||||||
|
widget.translations.chooseDateAndTime,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
FlutterFormInputDropdown(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.keyboard_arrow_down,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
isDense: true,
|
||||||
|
decoration: dropdownInputDecoration(
|
||||||
|
widget.translations.selectDay,
|
||||||
|
),
|
||||||
|
validationMessage: widget.translations.selectDayError,
|
||||||
|
controller: FlutterFormInputDropdownController(
|
||||||
|
id: "date",
|
||||||
|
mandatory: true,
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: widget.translations.dayToday,
|
||||||
|
child: Text(
|
||||||
|
widget.translations.dayToday,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: widget.translations.dayTomorrow,
|
||||||
|
child: Text(
|
||||||
|
widget.translations.dayTomorrow,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: ValueListenableBuilder(
|
||||||
|
valueListenable: switchStatus,
|
||||||
|
builder: (context, value, child) => AnimatedToggle(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 5,
|
||||||
|
color:
|
||||||
|
theme.colorScheme.primary.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
),
|
||||||
|
width: 280,
|
||||||
|
toggleColor: theme.colorScheme.primary,
|
||||||
|
onSwitch: (value) {
|
||||||
|
switchStatus.value = !switchStatus.value;
|
||||||
|
},
|
||||||
|
childLeft: Center(
|
||||||
|
child: Text(
|
||||||
|
widget.translations.morning,
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: switchStatus.value
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
childRight: Center(
|
||||||
|
child: Text(
|
||||||
|
widget.translations.afternoon,
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: switchStatus.value
|
||||||
|
? Colors.white
|
||||||
|
: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
ValueListenableBuilder(
|
||||||
|
valueListenable: switchStatus,
|
||||||
|
builder: (context, value, child) =>
|
||||||
|
FlutterFormInputMultipleChoice(
|
||||||
|
validationMessage: widget.translations.selectTime,
|
||||||
|
controller: multipleChoiceController,
|
||||||
|
options:
|
||||||
|
switchStatus.value ? afternoonTimes : morningTimes,
|
||||||
|
mainAxisSpacing: 5,
|
||||||
|
crossAxisSpacing: 5,
|
||||||
|
childAspectRatio: 2,
|
||||||
|
height: MediaQuery.of(context).size.height * 0.6,
|
||||||
|
builder: (
|
||||||
|
context,
|
||||||
|
index,
|
||||||
|
selected,
|
||||||
|
controller,
|
||||||
|
options,
|
||||||
|
state,
|
||||||
|
) =>
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
state.didChange(options[index]);
|
||||||
|
selected.value = index;
|
||||||
|
controller.onSaved(options[index]);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected.value == index
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
height: 40,
|
||||||
|
child: Center(
|
||||||
|
child: Text(options[index]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
nextButton: (pageNumber, checkingPages) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 32),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: PrimaryButton(
|
||||||
|
text: widget.translations.personalInformationButton,
|
||||||
|
onPressed: () async {
|
||||||
|
await dateTimeInformationController.autoNextStep();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onFinished: (values) {
|
||||||
|
widget.onFinished(values.entries.first.value);
|
||||||
|
},
|
||||||
|
onNext: (page, values) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
106
packages/flutter_shopping/lib/src/screens/filter_screen.dart
Normal file
106
packages/flutter_shopping/lib/src/screens/filter_screen.dart
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
import "package:rxdart/rxdart.dart";
|
||||||
|
|
||||||
|
/// filter screen
|
||||||
|
class FilterScreen extends StatelessWidget {
|
||||||
|
/// Constructor for the FilterScreen.
|
||||||
|
const FilterScreen({
|
||||||
|
required this.shoppingService,
|
||||||
|
required this.translations,
|
||||||
|
required this.options,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The shopping service.
|
||||||
|
final ShoppingService shoppingService;
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
/// The options.
|
||||||
|
final FlutterShoppingOptions options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: options.filterScreenAppbarBuilder?.call(
|
||||||
|
context,
|
||||||
|
translations.filterTitle,
|
||||||
|
() {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
) ??
|
||||||
|
AppBar(
|
||||||
|
leading: const SizedBox.shrink(),
|
||||||
|
title: Text(translations.filterTitle),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.close_rounded,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: StreamBuilder<List<dynamic>>(
|
||||||
|
stream: Rx.combineLatest(
|
||||||
|
[
|
||||||
|
shoppingService.getCategories(),
|
||||||
|
shoppingService.getSelectedCategoryStream(),
|
||||||
|
],
|
||||||
|
(List<dynamic> data) => data,
|
||||||
|
),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
var categories = snapshot.data![0] as List<Category>;
|
||||||
|
var selectedCategories = snapshot.data![1] as List<Category>;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
...categories.map(
|
||||||
|
(category) {
|
||||||
|
var isSelected = containsCategoryById(
|
||||||
|
selectedCategories,
|
||||||
|
category.id,
|
||||||
|
);
|
||||||
|
return options.filterItemBuilder?.call(
|
||||||
|
context,
|
||||||
|
() {
|
||||||
|
shoppingService.deselectCategory(category.id);
|
||||||
|
},
|
||||||
|
() {
|
||||||
|
shoppingService.selectCategory(category.id);
|
||||||
|
},
|
||||||
|
isSelected,
|
||||||
|
) ??
|
||||||
|
FilterItem(
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value) {
|
||||||
|
shoppingService.selectCategory(category.id);
|
||||||
|
} else {
|
||||||
|
shoppingService.deselectCategory(category.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
category: category,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the category is selected.
|
||||||
|
bool containsCategoryById(List<Category> categories, String id) =>
|
||||||
|
categories.any((category) => category.id == id);
|
|
@ -0,0 +1,118 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_form_wizard/flutter_form.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// A screen for the payment options.
|
||||||
|
class PaymentOptionsScreen extends StatelessWidget {
|
||||||
|
/// Constructor for the PaymentOptionsScreen.
|
||||||
|
const PaymentOptionsScreen({
|
||||||
|
required this.translations,
|
||||||
|
required this.onFinished,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
/// The on finished function.
|
||||||
|
final Function(Map<String, dynamic>) onFinished;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
var paymentOptionsController = FlutterFormController();
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(translations.paymentOptionsTitle),
|
||||||
|
),
|
||||||
|
body: FlutterForm(
|
||||||
|
formController: paymentOptionsController,
|
||||||
|
options: FlutterFormOptions(
|
||||||
|
pages: [
|
||||||
|
FlutterFormPage(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translations.paymenttitle,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
translations.paymentExplainer,
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 84,
|
||||||
|
),
|
||||||
|
FlutterFormInputMultipleChoice(
|
||||||
|
crossAxisCount: 1,
|
||||||
|
mainAxisSpacing: 24,
|
||||||
|
crossAxisSpacing: 5,
|
||||||
|
childAspectRatio: 2,
|
||||||
|
height: 422,
|
||||||
|
controller: FlutterFormInputMultipleChoiceController(
|
||||||
|
id: "payment",
|
||||||
|
mandatory: true,
|
||||||
|
),
|
||||||
|
options: [translations.payNow, translations.payLater],
|
||||||
|
builder: (
|
||||||
|
context,
|
||||||
|
index,
|
||||||
|
selected,
|
||||||
|
controller,
|
||||||
|
options,
|
||||||
|
state,
|
||||||
|
) =>
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
state.didChange(options[index]);
|
||||||
|
selected.value = index;
|
||||||
|
controller.onSaved(options[index]);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected.value == index
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
height: 40,
|
||||||
|
child: Center(child: Text(options[index])),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validationMessage: translations.paymentError,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
nextButton: (pageNumber, checkingPages) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 32),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: PrimaryButton(
|
||||||
|
text: translations.paymentButton,
|
||||||
|
onPressed: () async {
|
||||||
|
await paymentOptionsController.autoNextStep();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onFinished: (values) {
|
||||||
|
onFinished(values.entries.first.value);
|
||||||
|
},
|
||||||
|
onNext: (page, values) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,256 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_form_wizard/flutter_form.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// The personal information page.
|
||||||
|
class PersonalInformationScreen extends StatelessWidget {
|
||||||
|
/// Constructor for the PersonalInformationScreen.
|
||||||
|
const PersonalInformationScreen({
|
||||||
|
required this.translations,
|
||||||
|
required this.onFinished,
|
||||||
|
required this.options,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
/// The on finished function.
|
||||||
|
final Function(Map<String, dynamic>) onFinished;
|
||||||
|
|
||||||
|
/// The options.
|
||||||
|
final FlutterShoppingOptions options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
var personalInforamtionController = FlutterFormController();
|
||||||
|
|
||||||
|
InputDecoration inputDecoration(String hint) => InputDecoration(
|
||||||
|
hintStyle: theme.textTheme.bodySmall,
|
||||||
|
hintText: hint,
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(translations.personalInformationTitle),
|
||||||
|
),
|
||||||
|
body: FlutterForm(
|
||||||
|
formController: personalInforamtionController,
|
||||||
|
options: FlutterFormOptions(
|
||||||
|
pages: [
|
||||||
|
FlutterFormPage(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translations.personalInformationName,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
FlutterFormInputPlainText(
|
||||||
|
decoration: options.informationInputDecoration
|
||||||
|
?.call(context, translations.nameHint) ??
|
||||||
|
inputDecoration(translations.nameHint),
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
controller: FlutterFormInputPlainTextController(
|
||||||
|
id: "name",
|
||||||
|
mandatory: true,
|
||||||
|
),
|
||||||
|
validationMessage: translations.nameRequired,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
translations.personalInformationAddress,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
FlutterFormInputPlainText(
|
||||||
|
decoration: options.informationInputDecoration
|
||||||
|
?.call(context, translations.addressHint) ??
|
||||||
|
inputDecoration(translations.addressHint),
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
controller: FlutterFormInputPlainTextController(
|
||||||
|
id: "street",
|
||||||
|
mandatory: true,
|
||||||
|
),
|
||||||
|
validationMessage: translations.addressRequired,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return translations.addressRequired;
|
||||||
|
}
|
||||||
|
var regex = RegExp(r"^[A-Za-z]+\s[0-9]{1,3}$");
|
||||||
|
if (!regex.hasMatch(value)) {
|
||||||
|
return translations.invalidAdress;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
FlutterFormInputPlainText(
|
||||||
|
decoration: options.informationInputDecoration
|
||||||
|
?.call(context, translations.postalCodeHint) ??
|
||||||
|
inputDecoration(translations.postalCodeHint),
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
controller: FlutterFormInputPlainTextController(
|
||||||
|
id: "postalCode",
|
||||||
|
mandatory: true,
|
||||||
|
),
|
||||||
|
validationMessage: translations.postalCodeRequired,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return translations.postalCodeRequired;
|
||||||
|
}
|
||||||
|
var regex = RegExp(r"^[0-9]{4}[A-Za-z]{2}$");
|
||||||
|
if (!regex.hasMatch(value)) {
|
||||||
|
return translations.invalidPostalCode;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
FlutterFormInputPlainText(
|
||||||
|
decoration: options.informationInputDecoration
|
||||||
|
?.call(context, translations.cityHint) ??
|
||||||
|
inputDecoration(translations.cityHint),
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
controller: FlutterFormInputPlainTextController(
|
||||||
|
id: "city",
|
||||||
|
mandatory: true,
|
||||||
|
),
|
||||||
|
validationMessage: translations.cityRequired,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
translations.personalInformationPhone,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
FlutterFormInputPhone(
|
||||||
|
numberFieldStyle: theme.textTheme.bodySmall,
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
decoration: options.informationInputDecoration
|
||||||
|
?.call(context, translations.phoneHint) ??
|
||||||
|
inputDecoration(translations.phoneHint),
|
||||||
|
controller: FlutterFormInputPhoneController(
|
||||||
|
id: "phone",
|
||||||
|
mandatory: true,
|
||||||
|
),
|
||||||
|
validationMessage: translations.phoneRequired,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.number!.isEmpty) {
|
||||||
|
return translations.phoneRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any spaces or hyphens from the input
|
||||||
|
var phoneNumber =
|
||||||
|
value.number!.replaceAll(RegExp(r"\s+|-"), "");
|
||||||
|
|
||||||
|
// Check the length of the remaining digits
|
||||||
|
if (phoneNumber.length != 10 &&
|
||||||
|
phoneNumber.length != 11) {
|
||||||
|
return translations.invalidPhoneLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all remaining characters are digits
|
||||||
|
if (!phoneNumber
|
||||||
|
.substring(1)
|
||||||
|
.contains(RegExp(r"^[0-9]*$"))) {
|
||||||
|
return translations.phoneContainsLettersError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all checks pass, return null (no error)
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
translations.personalInformationEmail,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
FlutterFormInputEmail(
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
decoration: options.informationInputDecoration
|
||||||
|
?.call(context, translations.emailHint) ??
|
||||||
|
inputDecoration(translations.emailHint),
|
||||||
|
controller: FlutterFormInputEmailController(
|
||||||
|
id: "email",
|
||||||
|
mandatory: true,
|
||||||
|
),
|
||||||
|
validationMessage: translations.emailRequired,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
translations.personalInformationComment,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
FlutterFormInputPlainText(
|
||||||
|
decoration: options.informationInputDecoration
|
||||||
|
?.call(context, translations.commentHint) ??
|
||||||
|
inputDecoration(translations.commentHint),
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
controller: FlutterFormInputPlainTextController(
|
||||||
|
id: "comments",
|
||||||
|
),
|
||||||
|
validationMessage: "",
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 100,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
nextButton: (pageNumber, checkingPages) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 32),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: PrimaryButton(
|
||||||
|
text: translations.personalInformationButton,
|
||||||
|
onPressed: () async {
|
||||||
|
await personalInforamtionController.autoNextStep();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onFinished: (values) {
|
||||||
|
onFinished(values.entries.first.value);
|
||||||
|
},
|
||||||
|
onNext: (page, values) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// The shopping cart screen.
|
||||||
|
class ShoppingCartScreen extends StatelessWidget {
|
||||||
|
/// Constructor for the shopping cart screen.
|
||||||
|
const ShoppingCartScreen({
|
||||||
|
required this.shoppingService,
|
||||||
|
required this.translations,
|
||||||
|
required this.onOrder,
|
||||||
|
required this.options,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The shopping service.
|
||||||
|
final ShoppingService shoppingService;
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
/// the on order callback.
|
||||||
|
final Function() onOrder;
|
||||||
|
|
||||||
|
/// The options.
|
||||||
|
final FlutterShoppingOptions options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(translations.shoppingCartTitle),
|
||||||
|
),
|
||||||
|
body: StreamBuilder(
|
||||||
|
stream: shoppingService.getShoppingCart(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
var cart = snapshot.data!;
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translations.shoppingCartProducts,
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: cart.products
|
||||||
|
.map(
|
||||||
|
(product) =>
|
||||||
|
options.shoppingCartItemBuilder?.call(
|
||||||
|
context,
|
||||||
|
product,
|
||||||
|
() async {
|
||||||
|
await shoppingService
|
||||||
|
.addProductToCart(product);
|
||||||
|
},
|
||||||
|
() async {
|
||||||
|
await shoppingService
|
||||||
|
.removeProductFromCart(product);
|
||||||
|
},
|
||||||
|
) ??
|
||||||
|
ShoppingCartItem(
|
||||||
|
options: options,
|
||||||
|
translations: translations,
|
||||||
|
product: product,
|
||||||
|
onAddToCart: (product) async {
|
||||||
|
await shoppingService
|
||||||
|
.addProductToCart(product);
|
||||||
|
},
|
||||||
|
onRemoveFromCart: (product) async {
|
||||||
|
await shoppingService
|
||||||
|
.removeProductFromCart(product);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 200,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: theme.scaffoldBackgroundColor,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: 32,
|
||||||
|
left: 64,
|
||||||
|
right: 64,
|
||||||
|
top: 16,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translations.shoppingCartTotal,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${translations.shoppingCartCurrency}"
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
|
" ${cart.totalAmountWithDiscount.toStringAsFixed(2)}",
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
options.primaryButtonBuilder?.call(
|
||||||
|
context,
|
||||||
|
cart.products.isNotEmpty,
|
||||||
|
translations.orderButton,
|
||||||
|
onOrder,
|
||||||
|
) ??
|
||||||
|
PrimaryButton(
|
||||||
|
enabled: cart.products.isNotEmpty,
|
||||||
|
text: translations.orderButton,
|
||||||
|
onPressed: onOrder,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
252
packages/flutter_shopping/lib/src/screens/shopping_screen.dart
Normal file
252
packages/flutter_shopping/lib/src/screens/shopping_screen.dart
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
import "package:collection/collection.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// The shopping screen.
|
||||||
|
class ShoppingScreen extends StatefulWidget {
|
||||||
|
/// Constructor for the shopping screen.
|
||||||
|
const ShoppingScreen({
|
||||||
|
required this.shoppingService,
|
||||||
|
required this.translations,
|
||||||
|
required this.onShoppingCartPressed,
|
||||||
|
required this.options,
|
||||||
|
this.onFilterPressed,
|
||||||
|
this.initialShopId,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The shopping service.
|
||||||
|
final ShoppingService shoppingService;
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
/// The initial shop id.
|
||||||
|
final String? initialShopId;
|
||||||
|
|
||||||
|
/// The on filter pressed function.
|
||||||
|
final Function()? onFilterPressed;
|
||||||
|
|
||||||
|
/// The on shopping cart pressed function.
|
||||||
|
final Function() onShoppingCartPressed;
|
||||||
|
|
||||||
|
/// The options for shopping.
|
||||||
|
final FlutterShoppingOptions options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShoppingScreen> createState() => _ShoppingScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShoppingScreenState extends State<ShoppingScreen> {
|
||||||
|
Shop? selectedShop;
|
||||||
|
List<Category> selectedCategories = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
var service = widget.shoppingService;
|
||||||
|
var options = widget.options;
|
||||||
|
return Scaffold(
|
||||||
|
drawer: options.shoppingScreenDrawer,
|
||||||
|
appBar: options.shoppingScreenAppbarBuilder?.call(
|
||||||
|
context,
|
||||||
|
selectedShop?.adress ?? "",
|
||||||
|
widget.onFilterPressed,
|
||||||
|
) ??
|
||||||
|
AppBar(
|
||||||
|
title: Text(
|
||||||
|
selectedShop?.adress ?? "",
|
||||||
|
style: theme.textTheme.headlineLarge,
|
||||||
|
),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.person_rounded,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: widget.onFilterPressed,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.filter_alt_rounded,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
StreamBuilder(
|
||||||
|
stream: widget.shoppingService.getShops(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
selectedShop ??= widget.shoppingService.selectShop(
|
||||||
|
widget.initialShopId ?? snapshot.data!.first.id,
|
||||||
|
);
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: ShopSelector(
|
||||||
|
shops: snapshot.data!,
|
||||||
|
options: options,
|
||||||
|
shoppingService: widget.shoppingService,
|
||||||
|
onSelected: (shop) {
|
||||||
|
service.selectShop(shop.id);
|
||||||
|
setState(() {
|
||||||
|
selectedShop = shop;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StreamBuilder(
|
||||||
|
stream: service.getSelectedCategoryStream(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
selectedCategories = snapshot.data!;
|
||||||
|
return Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
children: snapshot.data!
|
||||||
|
.map(
|
||||||
|
(category) =>
|
||||||
|
options.filterChipbuilder?.call(
|
||||||
|
context, category.name, () {
|
||||||
|
service
|
||||||
|
.deselectCategory(category.id);
|
||||||
|
}) ??
|
||||||
|
FilterChip(
|
||||||
|
backgroundColor: theme.primaryColor,
|
||||||
|
onSelected: (value) {},
|
||||||
|
label: Text(
|
||||||
|
category.name,
|
||||||
|
style: theme.textTheme.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
deleteIcon: const Icon(
|
||||||
|
Icons.close_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onDeleted: () {
|
||||||
|
service.deselectCategory(
|
||||||
|
category.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
StreamBuilder(
|
||||||
|
stream: widget.shoppingService
|
||||||
|
.getProducts(selectedShop!.id),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
var products = groupBy(
|
||||||
|
snapshot.data!,
|
||||||
|
(Product p) => p.category,
|
||||||
|
);
|
||||||
|
var weeklyOffer = service.getWeeklyOffer();
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 32,
|
||||||
|
vertical: 20,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (weeklyOffer != null) ...[
|
||||||
|
options.weeklyOfferBuilder?.call(
|
||||||
|
context,
|
||||||
|
weeklyOffer,
|
||||||
|
) ??
|
||||||
|
WeeklyOffer(
|
||||||
|
product: weeklyOffer,
|
||||||
|
translations: widget.translations,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.translations
|
||||||
|
.whatWouldyouLikeToOrder,
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 20,
|
||||||
|
bottom: 100,
|
||||||
|
),
|
||||||
|
child: ProductItemList(
|
||||||
|
options: widget.options,
|
||||||
|
translations: widget.translations,
|
||||||
|
products: products,
|
||||||
|
onAddToCart: (product) async {
|
||||||
|
await service.addProductToCart(product);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
StreamBuilder(
|
||||||
|
stream: service.getCartLength(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
var cartLength = snapshot.data ?? 0;
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 32),
|
||||||
|
child: options.primaryButtonBuilder?.call(
|
||||||
|
context,
|
||||||
|
cartLength > 0,
|
||||||
|
widget.translations.viewShoppingCart, () {
|
||||||
|
widget.onShoppingCartPressed();
|
||||||
|
}) ??
|
||||||
|
PrimaryButton(
|
||||||
|
enabled: cartLength > 0,
|
||||||
|
text: widget.translations.viewShoppingCart,
|
||||||
|
onPressed: widget.onShoppingCartPressed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
44
packages/flutter_shopping/lib/src/widgets/filter_item.dart
Normal file
44
packages/flutter_shopping/lib/src/widgets/filter_item.dart
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// ignore_for_file: avoid_positional_boolean_parameters
|
||||||
|
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:shopping_repository_interface/shopping_repository_interface.dart";
|
||||||
|
|
||||||
|
/// A filter item.
|
||||||
|
class FilterItem extends StatelessWidget {
|
||||||
|
/// Constructor for the filter item.
|
||||||
|
const FilterItem({
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
required this.category,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The category.
|
||||||
|
final Category category;
|
||||||
|
|
||||||
|
/// The value.
|
||||||
|
final bool value;
|
||||||
|
|
||||||
|
/// The on changed function.
|
||||||
|
final Function(bool value) onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: CheckboxListTile(
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
value: value,
|
||||||
|
onChanged: (value) {
|
||||||
|
onChanged(value ?? false);
|
||||||
|
},
|
||||||
|
shape: const UnderlineInputBorder(),
|
||||||
|
title: Text(
|
||||||
|
category.name,
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// A bottomsheet that shows information.
|
||||||
|
class InfoBottomsheet extends StatelessWidget {
|
||||||
|
/// Constructor for the InfoBottomsheet.
|
||||||
|
const InfoBottomsheet({
|
||||||
|
required this.productInfo,
|
||||||
|
required this.translations,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The product info.
|
||||||
|
final String productInfo;
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
|
||||||
|
child: Text(
|
||||||
|
productInfo,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: PrimaryButton(
|
||||||
|
text: translations.closeInfo,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
/// A primary button.
|
||||||
|
class PrimaryButton extends StatelessWidget {
|
||||||
|
/// Constructor for the primary button.
|
||||||
|
const PrimaryButton({
|
||||||
|
required this.text,
|
||||||
|
required this.onPressed,
|
||||||
|
this.enabled = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The text.
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
/// The on pressed function.
|
||||||
|
final Function() onPressed;
|
||||||
|
|
||||||
|
/// whether the button is enabled.
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 254,
|
||||||
|
),
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: enabled ? onPressed : null,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: theme.textTheme.displayLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
139
packages/flutter_shopping/lib/src/widgets/product_item.dart
Normal file
139
packages/flutter_shopping/lib/src/widgets/product_item.dart
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import "package:cached_network_image/cached_network_image.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// Shop item.
|
||||||
|
class ProductItem extends StatelessWidget {
|
||||||
|
/// Constructor for the shop item.
|
||||||
|
const ProductItem({
|
||||||
|
required this.product,
|
||||||
|
required this.onAddToCart,
|
||||||
|
required this.translations,
|
||||||
|
required this.options,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The product.
|
||||||
|
final Product product;
|
||||||
|
|
||||||
|
/// The on add to cart function.
|
||||||
|
final Function(Product product) onAddToCart;
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
/// The options.
|
||||||
|
final FlutterShoppingOptions options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
image: DecorationImage(
|
||||||
|
image: CachedNetworkImageProvider(
|
||||||
|
product.imageUrl,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
product.name,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
options.onInfoPressed?.call(product) ??
|
||||||
|
showBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (context) => InfoBottomsheet(
|
||||||
|
productInfo: product.description,
|
||||||
|
translations: translations,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.info_outline_rounded,
|
||||||
|
color: theme.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (product.isDiscounted) ...[
|
||||||
|
Text(
|
||||||
|
product.price.toStringAsFixed(2),
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 30,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
product.isDiscounted
|
||||||
|
? product.discountPrice.toStringAsFixed(2)
|
||||||
|
: product.price.toStringAsFixed(2),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(fontSize: 14),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
await onAddToCart(product);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.primaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
"+",
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// ProductItemList
|
||||||
|
class ProductItemList extends StatelessWidget {
|
||||||
|
/// Constructor for the product item list.
|
||||||
|
const ProductItemList({
|
||||||
|
required this.products,
|
||||||
|
required this.onAddToCart,
|
||||||
|
required this.translations,
|
||||||
|
required this.options,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The items.
|
||||||
|
final Map<String, List<Product>> products;
|
||||||
|
|
||||||
|
/// The on add to cart function.
|
||||||
|
final Function(Product) onAddToCart;
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
/// The options.
|
||||||
|
final FlutterShoppingOptions options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Column(
|
||||||
|
children: products.entries
|
||||||
|
.map(
|
||||||
|
(entry) => Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
entry.key,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: entry.value
|
||||||
|
.map(
|
||||||
|
(product) =>
|
||||||
|
options.productItemBuilder?.call(
|
||||||
|
context,
|
||||||
|
product,
|
||||||
|
) ??
|
||||||
|
ProductItem(
|
||||||
|
options: options,
|
||||||
|
product: product,
|
||||||
|
translations: translations,
|
||||||
|
onAddToCart: (product) async {
|
||||||
|
await onAddToCart(product);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
53
packages/flutter_shopping/lib/src/widgets/shop_selector.dart
Normal file
53
packages/flutter_shopping/lib/src/widgets/shop_selector.dart
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// Shop selector.
|
||||||
|
class ShopSelector extends StatelessWidget {
|
||||||
|
/// Constructor for the shop selector.
|
||||||
|
const ShopSelector({
|
||||||
|
required this.shoppingService,
|
||||||
|
required this.onSelected,
|
||||||
|
required this.options,
|
||||||
|
required this.shops,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The shopping service.
|
||||||
|
final ShoppingService shoppingService;
|
||||||
|
|
||||||
|
/// The on selected function.
|
||||||
|
final Function(Shop) onSelected;
|
||||||
|
|
||||||
|
/// The options.
|
||||||
|
final FlutterShoppingOptions options;
|
||||||
|
|
||||||
|
///
|
||||||
|
final List<Shop> shops;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: shops
|
||||||
|
.map(
|
||||||
|
(shop) => Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
options.shopSelectorItemBuilder
|
||||||
|
?.call(context, shop, onSelected) ??
|
||||||
|
ShopSelectorItem(
|
||||||
|
shoppingService: shoppingService,
|
||||||
|
onSelected: onSelected,
|
||||||
|
shop: shop,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:shopping_repository_interface/shopping_repository_interface.dart";
|
||||||
|
|
||||||
|
/// Shop selector item.
|
||||||
|
class ShopSelectorItem extends StatelessWidget {
|
||||||
|
/// Constructor for the shop selector item.
|
||||||
|
const ShopSelectorItem({
|
||||||
|
required this.shop,
|
||||||
|
required this.onSelected,
|
||||||
|
required this.shoppingService,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The shop.
|
||||||
|
final Shop shop;
|
||||||
|
|
||||||
|
/// The on selected function.
|
||||||
|
final Function(Shop) onSelected;
|
||||||
|
|
||||||
|
/// The shopping service.
|
||||||
|
final ShoppingService shoppingService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
var isSelected = shop.id == shoppingService.getSelectedShop()?.id;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => onSelected(shop),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: !isSelected ? Border.all(color: theme.primaryColor) : null,
|
||||||
|
color: isSelected ? theme.primaryColor : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text(
|
||||||
|
shop.name,
|
||||||
|
style: isSelected
|
||||||
|
? theme.textTheme.titleMedium?.copyWith(color: Colors.white)
|
||||||
|
: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
import "package:cached_network_image/cached_network_image.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// The shopping cart item.
|
||||||
|
class ShoppingCartItem extends StatelessWidget {
|
||||||
|
/// Constructor for the shopping cart item.
|
||||||
|
const ShoppingCartItem({
|
||||||
|
required this.product,
|
||||||
|
required this.onAddToCart,
|
||||||
|
required this.onRemoveFromCart,
|
||||||
|
required this.translations,
|
||||||
|
required this.options,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The product.
|
||||||
|
final Product product;
|
||||||
|
|
||||||
|
/// The on add to cart function.
|
||||||
|
final Function(Product product) onAddToCart;
|
||||||
|
|
||||||
|
/// The on remove from cart function.
|
||||||
|
final Function(Product product) onRemoveFromCart;
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
/// The options.
|
||||||
|
final FlutterShoppingOptions options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
image: DecorationImage(
|
||||||
|
image: CachedNetworkImageProvider(
|
||||||
|
product.imageUrl,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
product.name,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
onPressed: () {
|
||||||
|
options.onInfoPressed?.call(product) ??
|
||||||
|
showBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (context) => InfoBottomsheet(
|
||||||
|
productInfo: product.description,
|
||||||
|
translations: translations,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.info_outline_rounded,
|
||||||
|
color: theme.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
product.isDiscounted
|
||||||
|
? product.discountPrice.toStringAsFixed(2)
|
||||||
|
: product.price.toStringAsFixed(2),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(fontSize: 14),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
onPressed: () async {
|
||||||
|
await onRemoveFromCart(product);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.remove_rounded),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
await onAddToCart(product);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.primaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
product.selectedAmount.toString(),
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
onPressed: () async {
|
||||||
|
await onAddToCart(product);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add_rounded),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
85
packages/flutter_shopping/lib/src/widgets/weekly_offer.dart
Normal file
85
packages/flutter_shopping/lib/src/widgets/weekly_offer.dart
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import "package:cached_network_image/cached_network_image.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// Weekly offer.
|
||||||
|
class WeeklyOffer extends StatelessWidget {
|
||||||
|
/// Constructor for the weekly offer.
|
||||||
|
const WeeklyOffer({
|
||||||
|
required this.product,
|
||||||
|
required this.translations,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The product.
|
||||||
|
final Product? product;
|
||||||
|
|
||||||
|
/// The translations.
|
||||||
|
final ShoppingTranslations translations;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 24),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 350,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.black,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
image: DecorationImage(
|
||||||
|
image: CachedNetworkImageProvider(
|
||||||
|
product!.imageUrl,
|
||||||
|
),
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.black,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(8),
|
||||||
|
topRight: Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
width: double.infinity,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
translations.weeklyOffer,
|
||||||
|
style: theme.textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(8),
|
||||||
|
bottomRight: Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
width: double.infinity,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
"${product!.name}, now for ${product!.discountPrice} each",
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,47 +1,37 @@
|
||||||
name: flutter_shopping
|
name: flutter_shopping
|
||||||
description: "A new Flutter project."
|
description: "A new Flutter package project."
|
||||||
publish_to: "none"
|
version: 3.0.0
|
||||||
version: 2.0.0
|
homepage: https://github.com/Iconica-Development/flutter_shopping/packages/flutter_shopping
|
||||||
|
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.4 <4.0.0"
|
sdk: ^3.5.1
|
||||||
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
go_router: any
|
cached_network_image: ^3.4.1
|
||||||
flutter_product_page:
|
rxdart: ^0.28.0
|
||||||
git:
|
collection: ^1.18.0
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
|
||||||
ref: 2.0.0
|
animated_toggle:
|
||||||
path: packages/flutter_product_page
|
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
|
||||||
flutter_shopping_cart:
|
version: ^0.0.3
|
||||||
git:
|
flutter_form_wizard:
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
|
||||||
ref: 2.0.0
|
version: ^6.6.0
|
||||||
path: packages/flutter_shopping_cart
|
|
||||||
flutter_order_details:
|
shopping_repository_interface:
|
||||||
git:
|
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
version: ^3.0.0
|
||||||
ref: 2.0.0
|
|
||||||
path: packages/flutter_order_details
|
|
||||||
flutter_shopping_interface:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
|
||||||
ref: 2.0.0
|
|
||||||
path: packages/flutter_shopping_interface
|
|
||||||
flutter_shopping_local:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
|
||||||
ref: 2.0.0
|
|
||||||
path: packages/flutter_shopping_local
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
|
||||||
sdk: flutter
|
|
||||||
flutter_iconica_analysis:
|
flutter_iconica_analysis:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
||||||
ref: 7.0.0
|
ref: 7.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
assets:
|
||||||
|
- assets/
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
# flutter_shopping_cart
|
|
||||||
|
|
||||||
This component contains a shopping cart screen and the functionality for shopping carts that contain products.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* Shopping cart screen
|
|
||||||
* Shopping cart products
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
First, create your own product by extending the `Product` class:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
class ExampleProduct extends Product {
|
|
||||||
ExampleProduct({
|
|
||||||
requried super.id,
|
|
||||||
required super.name,
|
|
||||||
required super.price,
|
|
||||||
required this.image,
|
|
||||||
super.quantity,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String image;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, you can create the `ShoppingCartScreen` widget like this:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
var myProductService = ProductService<ExampleProduct>([]);
|
|
||||||
|
|
||||||
ShoppingCartScreen<ExampleProduct>(
|
|
||||||
configuration: ShoppingCartConfig<ExampleProduct>(
|
|
||||||
productService: myProductService,
|
|
||||||
//
|
|
||||||
productItemBuilder: (
|
|
||||||
BuildContext context,
|
|
||||||
Locale locale,
|
|
||||||
ExampleProduct product,
|
|
||||||
) =>
|
|
||||||
ListTile(
|
|
||||||
title: Text(product.name),
|
|
||||||
subtitle: Text(product.price.toString()),
|
|
||||||
),
|
|
||||||
//
|
|
||||||
onConfirmOrder: (List<ExampleProduct> products) {
|
|
||||||
print("Placing order with products: $products");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping_cart/tree/main/example).
|
|
||||||
|
|
||||||
Or, you could run the example yourself:
|
|
||||||
```
|
|
||||||
git clone https://github.com/Iconica-Development/flutter_shopping_cart.git
|
|
||||||
|
|
||||||
cd flutter_shopping_cart
|
|
||||||
|
|
||||||
cd example
|
|
||||||
|
|
||||||
flutter run
|
|
||||||
```
|
|
||||||
|
|
||||||
## Issues
|
|
||||||
|
|
||||||
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_shopping_cart) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
|
|
||||||
|
|
||||||
## Want to contribute
|
|
||||||
|
|
||||||
If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_shopping_cart/pulls).
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
This flutter_shopping_cart for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
|
|
|
@ -1,9 +0,0 @@
|
||||||
include: package:flutter_iconica_analysis/components_options.yaml
|
|
||||||
|
|
||||||
# Possible to overwrite the rules from the package
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
|
@ -1,6 +0,0 @@
|
||||||
/// Flutter component for shopping cart.
|
|
||||||
library flutter_shopping_cart;
|
|
||||||
|
|
||||||
export "src/config/shopping_cart_config.dart";
|
|
||||||
export "src/config/shopping_cart_translations.dart";
|
|
||||||
export "src/widgets/shopping_cart_screen.dart";
|
|
|
@ -1,89 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Shopping cart configuration
|
|
||||||
///
|
|
||||||
/// This class is used to configure the shopping cart.
|
|
||||||
class ShoppingCartConfig {
|
|
||||||
/// Creates a shopping cart configuration.
|
|
||||||
ShoppingCartConfig({
|
|
||||||
required this.service,
|
|
||||||
required this.onConfirmOrder,
|
|
||||||
this.productItemBuilder,
|
|
||||||
this.confirmOrderButtonBuilder,
|
|
||||||
this.confirmOrderButtonHeight = 100,
|
|
||||||
this.sumBottomSheetBuilder,
|
|
||||||
this.sumBottomSheetHeight = 100,
|
|
||||||
this.titleBuilder,
|
|
||||||
this.translations = const ShoppingCartTranslations(),
|
|
||||||
this.pagePadding = const EdgeInsets.symmetric(horizontal: 32),
|
|
||||||
this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32),
|
|
||||||
this.appBarBuilder,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Product service. The product service is used to manage the products in the
|
|
||||||
/// shopping cart.
|
|
||||||
final ShoppingCartService service;
|
|
||||||
|
|
||||||
/// Product item builder. This builder is used to build the product item
|
|
||||||
/// that will be displayed in the shopping cart.
|
|
||||||
Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
Product product,
|
|
||||||
ShoppingCartConfig configuration,
|
|
||||||
)? productItemBuilder;
|
|
||||||
|
|
||||||
/// Confirm order button builder. This builder is used to build the confirm
|
|
||||||
/// order button that will be displayed in the shopping cart.
|
|
||||||
/// If you override this builder, you cannot use the [onConfirmOrder] callback
|
|
||||||
Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
ShoppingCartConfig configuration,
|
|
||||||
Function(List<Product> products) onConfirmOrder,
|
|
||||||
)? confirmOrderButtonBuilder;
|
|
||||||
|
|
||||||
/// Confirm order button height. The height of the confirm order button.
|
|
||||||
/// This height is used to calculate the bottom padding of the shopping cart.
|
|
||||||
/// If you override the confirm order button builder, you must provide a
|
|
||||||
/// height.
|
|
||||||
double confirmOrderButtonHeight;
|
|
||||||
|
|
||||||
/// Confirm order callback. This callback is called when the confirm order
|
|
||||||
/// button is pressed. The callback will not be called if you override the
|
|
||||||
/// confirm order button builder.
|
|
||||||
final Function(List<Product> products) onConfirmOrder;
|
|
||||||
|
|
||||||
/// Sum bottom sheet builder. This builder is used to build the sum bottom
|
|
||||||
/// sheet that will be displayed in the shopping cart. The sum bottom sheet
|
|
||||||
/// can be used to display the total sum of the products in the shopping cart.
|
|
||||||
Widget Function(BuildContext context, ShoppingCartConfig configuration)?
|
|
||||||
sumBottomSheetBuilder;
|
|
||||||
|
|
||||||
/// Sum bottom sheet height. The height of the sum bottom sheet.
|
|
||||||
/// This height is used to calculate the bottom padding of the shopping cart.
|
|
||||||
/// If you override the sum bottom sheet builder, you must provide a height.
|
|
||||||
double sumBottomSheetHeight;
|
|
||||||
|
|
||||||
/// Padding around the shopping cart. The padding is used to create space
|
|
||||||
/// around the shopping cart.
|
|
||||||
EdgeInsets pagePadding;
|
|
||||||
|
|
||||||
/// Bottom padding of the shopping cart. The bottom padding is used to create
|
|
||||||
/// a padding around the bottom sheet. This padding is ignored when the
|
|
||||||
/// [sumBottomSheetBuilder] is overridden.
|
|
||||||
EdgeInsets bottomPadding;
|
|
||||||
|
|
||||||
/// Title builder. This builder is used to
|
|
||||||
/// build the title of the shopping cart.
|
|
||||||
final Widget Function(
|
|
||||||
BuildContext context,
|
|
||||||
String title,
|
|
||||||
)? titleBuilder;
|
|
||||||
|
|
||||||
/// Shopping cart translations. The translations for the shopping cart.
|
|
||||||
ShoppingCartTranslations translations;
|
|
||||||
|
|
||||||
/// Appbar for the shopping cart screen.
|
|
||||||
PreferredSizeWidget Function(BuildContext context)? appBarBuilder;
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
/// Shopping cart localizations
|
|
||||||
class ShoppingCartTranslations {
|
|
||||||
/// Creates shopping cart localizations
|
|
||||||
const ShoppingCartTranslations({
|
|
||||||
this.placeOrder = "Order",
|
|
||||||
this.sum = "Subtotal:",
|
|
||||||
this.cartTitle = "Products",
|
|
||||||
this.close = "close",
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Text for the place order button.
|
|
||||||
final String placeOrder;
|
|
||||||
|
|
||||||
/// Localization for the sum.
|
|
||||||
final String sum;
|
|
||||||
|
|
||||||
/// Title for the shopping cart. This title will be displayed at the top of
|
|
||||||
/// the shopping cart.
|
|
||||||
final String cartTitle;
|
|
||||||
|
|
||||||
/// Localization for the close button for the popup.
|
|
||||||
final String close;
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
|
|
||||||
/// Default appbar for the shopping cart.
|
|
||||||
class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget {
|
|
||||||
/// Constructor for the default appbar for the shopping cart.
|
|
||||||
const DefaultAppbar({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return AppBar(
|
|
||||||
title: Text(
|
|
||||||
"Shopping cart",
|
|
||||||
style: theme.textTheme.headlineLarge,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Default confirm order button.
|
|
||||||
class DefaultConfirmOrderButton extends StatelessWidget {
|
|
||||||
/// Constructor for the default confirm order button.
|
|
||||||
const DefaultConfirmOrderButton({
|
|
||||||
required this.configuration,
|
|
||||||
required this.onConfirmOrder,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configuration for the shopping cart.
|
|
||||||
final ShoppingCartConfig configuration;
|
|
||||||
|
|
||||||
/// Function to call when the order is confirmed.
|
|
||||||
final Function(List<Product> products) onConfirmOrder;
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 60),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: configuration.service.products.isEmpty
|
|
||||||
? null
|
|
||||||
: () => onConfirmOrder(
|
|
||||||
configuration.service.products,
|
|
||||||
),
|
|
||||||
style: theme.filledButtonTheme.style?.copyWith(
|
|
||||||
backgroundColor: WidgetStateProperty.all(
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
configuration.translations.placeOrder,
|
|
||||||
style: theme.textTheme.displayLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
|
||||||
import "package:flutter_shopping_cart/src/widgets/product_item_popup.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Default shopping cart item.
|
|
||||||
class DefaultShoppingCartItem extends StatelessWidget {
|
|
||||||
/// Constructor for the default shopping cart item.
|
|
||||||
const DefaultShoppingCartItem({
|
|
||||||
required this.product,
|
|
||||||
required this.configuration,
|
|
||||||
required this.onItemAddedRemoved,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Product to display.
|
|
||||||
final Product product;
|
|
||||||
|
|
||||||
/// Shopping cart configuration.
|
|
||||||
final ShoppingCartConfig configuration;
|
|
||||||
|
|
||||||
/// Function that is called when an item is added or removed.
|
|
||||||
final Function() onItemAddedRemoved;
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 20),
|
|
||||||
child: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.only(top: 3, left: 4, bottom: 3),
|
|
||||||
title: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
product.name,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
onPressed: () async {
|
|
||||||
await showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
backgroundColor: theme.colorScheme.surface,
|
|
||||||
builder: (context) => ProductItemPopup(
|
|
||||||
product: product,
|
|
||||||
configuration: configuration,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
Icons.info_outline,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
leading: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
child: Image.network(
|
|
||||||
product.imageUrl,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (product.hasDiscount && product.discountPrice != null) ...[
|
|
||||||
Text(
|
|
||||||
product.discountPrice!.toStringAsFixed(2),
|
|
||||||
style: theme.textTheme.labelSmall,
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
Text(
|
|
||||||
product.price.toStringAsFixed(2),
|
|
||||||
style: theme.textTheme.labelSmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.remove,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
configuration.service.removeOneProduct(product);
|
|
||||||
onItemAddedRemoved();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(2),
|
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
height: 30,
|
|
||||||
width: 30,
|
|
||||||
child: Text(
|
|
||||||
"${product.quantity}",
|
|
||||||
style: theme.textTheme.titleSmall,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.add,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
configuration.service.addProduct(product);
|
|
||||||
onItemAddedRemoved();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
|
||||||
|
|
||||||
/// Default sum bottom sheet builder.
|
|
||||||
class DefaultSumBottomSheetBuilder extends StatelessWidget {
|
|
||||||
/// Constructor for the default sum bottom sheet builder.
|
|
||||||
const DefaultSumBottomSheetBuilder({
|
|
||||||
required this.configuration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configuration for the shopping cart.
|
|
||||||
final ShoppingCartConfig configuration;
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
var totalPrice = configuration.service.products.fold<double>(
|
|
||||||
0,
|
|
||||||
(previousValue, element) =>
|
|
||||||
previousValue +
|
|
||||||
(element.discountPrice ?? element.price) * element.quantity,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: configuration.bottomPadding,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
configuration.translations.sum,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Text(
|
|
||||||
"€ ${totalPrice.toStringAsFixed(2)}",
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.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 Product product;
|
|
||||||
|
|
||||||
/// Configuration for the product page.
|
|
||||||
final ShoppingCartConfig configuration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
product.description,
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 20, left: 40, right: 40),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
style: theme.filledButtonTheme.style?.copyWith(
|
|
||||||
backgroundColor: WidgetStateProperty.all(
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0,
|
|
||||||
vertical: 8.0,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
configuration.translations.close,
|
|
||||||
style: theme.textTheme.displayLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
|
||||||
import "package:flutter_shopping_cart/src/widgets/default_appbar.dart";
|
|
||||||
import "package:flutter_shopping_cart/src/widgets/default_confirm_order_button.dart";
|
|
||||||
import "package:flutter_shopping_cart/src/widgets/default_shopping_cart_item.dart";
|
|
||||||
import "package:flutter_shopping_cart/src/widgets/default_sum_bottom_sheet_builder.dart";
|
|
||||||
|
|
||||||
/// Shopping cart screen widget.
|
|
||||||
class ShoppingCartScreen extends StatefulWidget {
|
|
||||||
/// Creates a shopping cart screen.
|
|
||||||
const ShoppingCartScreen({
|
|
||||||
required this.configuration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configuration for the shopping cart screen.
|
|
||||||
final ShoppingCartConfig configuration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ShoppingCartScreen> createState() => _ShoppingCartScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ShoppingCartScreenState extends State<ShoppingCartScreen> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: widget.configuration.appBarBuilder?.call(context) ??
|
|
||||||
const DefaultAppbar(),
|
|
||||||
body: SafeArea(
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: widget.configuration.pagePadding,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
if (widget.configuration.titleBuilder != null) ...{
|
|
||||||
widget.configuration.titleBuilder!(
|
|
||||||
context,
|
|
||||||
widget.configuration.translations.cartTitle,
|
|
||||||
),
|
|
||||||
} else ...{
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 32,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.configuration.translations.cartTitle,
|
|
||||||
style: theme.textTheme.titleLarge,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
for (var product
|
|
||||||
in widget.configuration.service.products)
|
|
||||||
widget.configuration.productItemBuilder?.call(
|
|
||||||
context,
|
|
||||||
product,
|
|
||||||
widget.configuration,
|
|
||||||
) ??
|
|
||||||
DefaultShoppingCartItem(
|
|
||||||
product: product,
|
|
||||||
configuration: widget.configuration,
|
|
||||||
onItemAddedRemoved: () {
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// Additional whitespace at
|
|
||||||
// the bottom to make sure the last
|
|
||||||
// product(s) are not hidden by the bottom sheet.
|
|
||||||
SizedBox(
|
|
||||||
height:
|
|
||||||
widget.configuration.confirmOrderButtonHeight +
|
|
||||||
widget.configuration.sumBottomSheetHeight,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: _BottomSheet(
|
|
||||||
configuration: widget.configuration,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BottomSheet extends StatelessWidget {
|
|
||||||
const _BottomSheet({
|
|
||||||
required this.configuration,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ShoppingCartConfig configuration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
configuration.sumBottomSheetBuilder?.call(context, configuration) ??
|
|
||||||
DefaultSumBottomSheetBuilder(
|
|
||||||
configuration: configuration,
|
|
||||||
),
|
|
||||||
configuration.confirmOrderButtonBuilder?.call(
|
|
||||||
context,
|
|
||||||
configuration,
|
|
||||||
configuration.onConfirmOrder,
|
|
||||||
) ??
|
|
||||||
DefaultConfirmOrderButton(
|
|
||||||
configuration: configuration,
|
|
||||||
onConfirmOrder: configuration.onConfirmOrder,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
name: flutter_shopping_cart
|
|
||||||
description: "A Flutter module for a shopping cart."
|
|
||||||
version: 2.0.0
|
|
||||||
publish_to: "none"
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
|
||||||
flutter: ">=1.17.0"
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
flutter_shopping_interface:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
|
||||||
path: packages/flutter_shopping_interface
|
|
||||||
ref: 2.0.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_test:
|
|
||||||
sdk: flutter
|
|
||||||
flutter_iconica_analysis:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
|
||||||
ref: 7.0.0
|
|
||||||
|
|
||||||
flutter:
|
|
|
@ -1,9 +0,0 @@
|
||||||
include: package:flutter_iconica_analysis/components_options.yaml
|
|
||||||
|
|
||||||
# Possible to overwrite the rules from the package
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
|
@ -1,2 +0,0 @@
|
||||||
export "src/model/model.dart";
|
|
||||||
export "src/service/service.dart";
|
|
|
@ -1,2 +0,0 @@
|
||||||
export "product.dart";
|
|
||||||
export "shop.dart";
|
|
|
@ -1,85 +0,0 @@
|
||||||
/// Product Interface
|
|
||||||
abstract class ProductInterface {
|
|
||||||
/// ProductInterface constructor
|
|
||||||
ProductInterface({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.imageUrl,
|
|
||||||
required this.category,
|
|
||||||
required this.price,
|
|
||||||
required this.description,
|
|
||||||
this.hasDiscount = false,
|
|
||||||
this.discountPrice,
|
|
||||||
this.quantity = 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Product id
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
/// Product name
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
/// Product image url
|
|
||||||
final String imageUrl;
|
|
||||||
|
|
||||||
/// Product category
|
|
||||||
final String category;
|
|
||||||
|
|
||||||
/// Product price
|
|
||||||
final double price;
|
|
||||||
|
|
||||||
/// whether the product has a discount
|
|
||||||
final bool hasDiscount;
|
|
||||||
|
|
||||||
/// Product discount price
|
|
||||||
final double? discountPrice;
|
|
||||||
|
|
||||||
/// Product quantity
|
|
||||||
int quantity;
|
|
||||||
|
|
||||||
/// Product description
|
|
||||||
final String description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Product model
|
|
||||||
class Product implements ProductInterface {
|
|
||||||
/// Product constructor
|
|
||||||
Product({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.imageUrl,
|
|
||||||
required this.category,
|
|
||||||
required this.price,
|
|
||||||
required this.description,
|
|
||||||
this.hasDiscount = false,
|
|
||||||
this.discountPrice,
|
|
||||||
this.quantity = 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String imageUrl;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String category;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final double price;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final bool hasDiscount;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final double? discountPrice;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int quantity;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String description;
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
/// Shop interface
|
|
||||||
abstract class ShopInterface {
|
|
||||||
/// ShopInterface constructor
|
|
||||||
const ShopInterface({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Shop id
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
/// Shop name
|
|
||||||
final String name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shop model
|
|
||||||
class Shop implements ShopInterface {
|
|
||||||
/// Shop constructor
|
|
||||||
const Shop({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
});
|
|
||||||
@override
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String name;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import "package:flutter_shopping_interface/src/model/product.dart";
|
|
||||||
|
|
||||||
/// Order service
|
|
||||||
// ignore: one_member_abstracts
|
|
||||||
abstract class OrderService {
|
|
||||||
/// Create an order
|
|
||||||
Future<void> createOrder(
|
|
||||||
String shopId,
|
|
||||||
List<Product> products,
|
|
||||||
Map<int, Map<String, dynamic>> clientInformation,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_interface/src/model/product.dart";
|
|
||||||
|
|
||||||
/// Product service
|
|
||||||
abstract class ProductService with ChangeNotifier {
|
|
||||||
/// Retrieve a list of products
|
|
||||||
Future<List<Product>> getProducts(String shopId);
|
|
||||||
|
|
||||||
/// Retrieve a product
|
|
||||||
Future<Product> getProduct(String id);
|
|
||||||
|
|
||||||
/// Retrieve a list of categories
|
|
||||||
List<String> getCategories();
|
|
||||||
|
|
||||||
/// Get current Products
|
|
||||||
List<Product> get products;
|
|
||||||
|
|
||||||
/// Get current Products
|
|
||||||
List<String> get selectedCategories;
|
|
||||||
|
|
||||||
/// Select a category
|
|
||||||
void selectCategory(String category);
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
export "order_service.dart";
|
|
||||||
export "product_service.dart";
|
|
||||||
export "shop_service.dart";
|
|
||||||
export "shopping_cart_service.dart";
|
|
||||||
export "shopping_service.dart";
|
|
|
@ -1,15 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_interface/src/model/shop.dart";
|
|
||||||
|
|
||||||
/// Shop service
|
|
||||||
// ignore: one_member_abstracts
|
|
||||||
abstract class ShopService with ChangeNotifier {
|
|
||||||
/// Retrieve a list of shops
|
|
||||||
Future<List<Shop>> getShops();
|
|
||||||
|
|
||||||
/// Select a shop
|
|
||||||
void selectShop(Shop shop);
|
|
||||||
|
|
||||||
/// The currently selected shop
|
|
||||||
Shop? get selectedShop;
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_interface/src/model/product.dart";
|
|
||||||
|
|
||||||
/// shopping cart service
|
|
||||||
abstract class ShoppingCartService with ChangeNotifier {
|
|
||||||
/// Adds a product to the shopping cart.
|
|
||||||
void addProduct(Product product);
|
|
||||||
|
|
||||||
/// Removes a product from the shopping cart.
|
|
||||||
void removeProduct(Product product);
|
|
||||||
|
|
||||||
/// Removes one product from the shopping cart.
|
|
||||||
void removeOneProduct(Product product);
|
|
||||||
|
|
||||||
/// Counts the number of products in the shopping cart.
|
|
||||||
int countProducts();
|
|
||||||
|
|
||||||
/// Clears the shopping cart.
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
/// The list of products in the shopping cart.
|
|
||||||
List<Product> get products;
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import "package:flutter_shopping_interface/src/service/service.dart";
|
|
||||||
|
|
||||||
/// Shopping service
|
|
||||||
class ShoppingService {
|
|
||||||
/// Shopping service constructor
|
|
||||||
const ShoppingService({
|
|
||||||
required this.orderService,
|
|
||||||
required this.productService,
|
|
||||||
required this.shopService,
|
|
||||||
required this.shoppingCartService,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Order service
|
|
||||||
final OrderService orderService;
|
|
||||||
|
|
||||||
/// Product service
|
|
||||||
final ProductService productService;
|
|
||||||
|
|
||||||
/// Shop service
|
|
||||||
final ShopService shopService;
|
|
||||||
|
|
||||||
/// Shopping cart service
|
|
||||||
final ShoppingCartService shoppingCartService;
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
name: flutter_shopping_interface
|
|
||||||
description: "A Flutter module for a shopping."
|
|
||||||
version: 2.0.0
|
|
||||||
publish_to: 'none'
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
|
||||||
flutter: ">=1.17.0"
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_test:
|
|
||||||
sdk: flutter
|
|
||||||
flutter_iconica_analysis:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
|
||||||
ref: 7.0.0
|
|
||||||
|
|
||||||
flutter:
|
|
|
@ -1,9 +0,0 @@
|
||||||
include: package:flutter_iconica_analysis/components_options.yaml
|
|
||||||
|
|
||||||
# Possible to overwrite the rules from the package
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
|
@ -1 +0,0 @@
|
||||||
export "service/service.dart";
|
|
|
@ -1,15 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Local order service
|
|
||||||
class LocalOrderService with ChangeNotifier implements OrderService {
|
|
||||||
@override
|
|
||||||
Future<void> createOrder(
|
|
||||||
String shopId,
|
|
||||||
List<Product> products,
|
|
||||||
Map<int, Map<String, dynamic>> clientInformation,
|
|
||||||
) async {
|
|
||||||
// Create the order
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Local product service
|
|
||||||
class LocalProductService with ChangeNotifier implements ProductService {
|
|
||||||
List<Product> _products = [];
|
|
||||||
List<Product> _allProducts = [];
|
|
||||||
final List<String> _selectedCategories = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> getCategories() =>
|
|
||||||
_allProducts.map((e) => e.category).toSet().toList();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Product> getProduct(String id) =>
|
|
||||||
Future.value(_products.firstWhere((element) => element.id == id));
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<Product>> getProducts(String shopId) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
_products = [
|
|
||||||
Product(
|
|
||||||
id: "1",
|
|
||||||
name: "White Bread",
|
|
||||||
imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshell-demo"
|
|
||||||
".appspot.com/o/shopping%2Fwhite.png"
|
|
||||||
"?alt=media&token=e3aa13d5-932a-4119-bdbb-89c1a1d82213",
|
|
||||||
category: "Bread",
|
|
||||||
price: 1.0,
|
|
||||||
description: "This is a delicious white bread",
|
|
||||||
hasDiscount: true,
|
|
||||||
discountPrice: 0.5,
|
|
||||||
),
|
|
||||||
Product(
|
|
||||||
id: "2",
|
|
||||||
name: "Brown Bread",
|
|
||||||
imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshell-"
|
|
||||||
"demo.appspot.com/o/shopping%2Fbrown.png?alt=media&"
|
|
||||||
"token=fbfe280d-44e6-4cde-a491-bcb7f598d22c",
|
|
||||||
category: "Bread",
|
|
||||||
price: 2.0,
|
|
||||||
description: "This is a delicious brown bread",
|
|
||||||
),
|
|
||||||
Product(
|
|
||||||
id: "3",
|
|
||||||
name: "White fish",
|
|
||||||
imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshell"
|
|
||||||
"-demo.appspot.com/o/shopping%2Fwhite-fish.png?alt=media"
|
|
||||||
"&token=61c44f84-347d-4b42-a10d-c172f167929b",
|
|
||||||
category: "Fish",
|
|
||||||
price: 1.5,
|
|
||||||
description: "This is a delicious white fish",
|
|
||||||
),
|
|
||||||
Product(
|
|
||||||
id: "4",
|
|
||||||
name: "Brown fish",
|
|
||||||
imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshel"
|
|
||||||
"l-demo.appspot.com/o/shopping%2Fbrown-fish.png?alt=media&"
|
|
||||||
"token=b743a53c-c4bb-49ac-8894-a08132992902",
|
|
||||||
category: "Fish",
|
|
||||||
price: 1.5,
|
|
||||||
description: "This is a delicious Brown fish",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// only return items that match the selectedcategories
|
|
||||||
_allProducts = List.from(_products);
|
|
||||||
|
|
||||||
_products = _products.where((element) {
|
|
||||||
if (_selectedCategories.isEmpty) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return _selectedCategories.contains(element.category);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
return Future.value(_products);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Product> get products => _products;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void selectCategory(String category) {
|
|
||||||
if (_selectedCategories.contains(category)) {
|
|
||||||
_selectedCategories.remove(category);
|
|
||||||
} else {
|
|
||||||
_selectedCategories.add(category);
|
|
||||||
}
|
|
||||||
if (_selectedCategories.isEmpty) {
|
|
||||||
_products = List.from(_allProducts);
|
|
||||||
}
|
|
||||||
_products = _allProducts.where((element) {
|
|
||||||
if (_selectedCategories.isEmpty) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return _selectedCategories.contains(element.category);
|
|
||||||
}).toList();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> get selectedCategories => _selectedCategories;
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// A service that provides a list of shops.
|
|
||||||
class LocalShopService with ChangeNotifier implements ShopService {
|
|
||||||
Shop? _selectedShop;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<Shop>> getShops() async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
var shops = <Shop>[
|
|
||||||
const Shop(id: "1", name: "Bakkerij de Goudkorst"),
|
|
||||||
const Shop(id: "2", name: "Slagerij PuurVlees"),
|
|
||||||
const Shop(id: "3", name: "De GroenteHut"),
|
|
||||||
const Shop(id: "4", name: "Pizzeria Ciao"),
|
|
||||||
const Shop(id: "5", name: "Cafetaria Roos"),
|
|
||||||
const Shop(id: "6", name: "Zeebries Visdelicatessen"),
|
|
||||||
const Shop(id: "7", name: "De Oosterse Draak"),
|
|
||||||
];
|
|
||||||
return Future.value(shops);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the selected shop.
|
|
||||||
@override
|
|
||||||
void selectShop(Shop shop) {
|
|
||||||
if (_selectedShop == shop) return;
|
|
||||||
|
|
||||||
_selectedShop = shop;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The currently selected shop.
|
|
||||||
@override
|
|
||||||
Shop? get selectedShop => _selectedShop;
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Local shopping cart service
|
|
||||||
class LocalShoppingCartService
|
|
||||||
with ChangeNotifier
|
|
||||||
implements ShoppingCartService {
|
|
||||||
final List<Product> _products = [];
|
|
||||||
@override
|
|
||||||
void addProduct(Product product) {
|
|
||||||
if (_products.contains(product)) {
|
|
||||||
var index = _products.indexOf(product);
|
|
||||||
_products[index].quantity++;
|
|
||||||
} else {
|
|
||||||
_products.add(product);
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void clear() {
|
|
||||||
_products.clear();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int countProducts() {
|
|
||||||
var count = 0;
|
|
||||||
for (var product in _products) {
|
|
||||||
count += product.quantity;
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeOneProduct(Product product) {
|
|
||||||
if (_products.contains(product)) {
|
|
||||||
var index = _products.indexOf(product);
|
|
||||||
if (_products[index].quantity > 1) {
|
|
||||||
_products[index].quantity--;
|
|
||||||
} else {
|
|
||||||
_products.removeAt(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeProduct(Product product) {
|
|
||||||
if (_products.contains(product)) {
|
|
||||||
var index = _products.indexOf(product);
|
|
||||||
_products.removeAt(index);
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Product> get products => _products;
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue